/*
  The MIT License (MIT)

  Copyright (c) 2016 Meetecho

  Permission is hereby granted, free of charge, to any person obtaining
  a copy of this software and associated documentation files (the "Software"),
  to deal in the Software without restriction, including without limitation
  the rights to use, copy, modify, merge, publish, distribute, sublicense,
  and/or sell copies of the Software, and to permit persons to whom the
  Software is furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included
  in all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  OTHER DEALINGS IN THE SOFTWARE.
 */

/* eslint-disable no-global-assign, no-redeclare, no-unused-vars, no-empty, no-inner-declarations */

import adapter from 'webrtc-adapter'

// List of sessions
Janus.sessions = {}

Janus.isExtensionEnabled = function() {
  if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
    // No need for the extension, getDisplayMedia is supported
    return true
  }
  if(window.navigator.userAgent.match('Chrome')) {
    var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10)
    var maxver = 33
    if(window.navigator.userAgent.match('Linux'))
      maxver = 35 // "known" crash in chrome 34 and 35 on linux
    if(chromever >= 26 && chromever <= maxver) {
      // Older versions of Chrome don't support this extension-based approach, so lie
      return true
    }
    return Janus.extension.isInstalled()
  } else {
    // Firefox and others, no need for the extension (but this doesn't mean it will work)
    return true
  }
}

var defaultExtension = {
  // Screensharing Chrome Extension ID
  extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj',
  isInstalled: function() { return document.querySelector('#janus-extension-installed') !== null },
  getScreen: function (callback) {
    var pending = window.setTimeout(function () {
      var error = new Error('NavigatorUserMediaError')
      error.name = 'The required Chrome extension is not installed: click <a href="#">here</a> to install it. (NOTE: this will need you to refresh the page)'
      return callback(error)
    }, 1000)
    this.cache[pending] = callback
    window.postMessage({ type: 'janusGetScreen', id: pending }, '*')
  },
  init: function () {
    var cache = {}
    this.cache = cache
    // Wait for events from the Chrome Extension
    window.addEventListener('message', function (event) {
      if(event.origin != window.location.origin)
        return
      if(event.data.type == 'janusGotScreen' && cache[event.data.id]) {
        var callback = cache[event.data.id]
        delete cache[event.data.id]

        if (event.data.sourceId === '') {
          // user canceled
          var error = new Error('NavigatorUserMediaError')
          error.name = 'You cancelled the request for permission, giving up...'
          callback(error)
        } else {
          callback(null, event.data.sourceId)
        }
      } else if (event.data.type == 'janusGetScreenPending') {
        console.log('clearing ', event.data.id)
        window.clearTimeout(event.data.id)
      }
    })
  }
}

Janus.useDefaultDependencies = function (deps) {
  var f = (deps && deps.fetch) || fetch
  var p = (deps && deps.Promise) || Promise
  var socketCls = (deps && deps.WebSocket) || WebSocket

  return {
    newWebSocket: function(server, proto) { return new socketCls(server, proto) },
    extension: (deps && deps.extension) || defaultExtension,
    isArray: function(arr) { return Array.isArray(arr) },
    webRTCAdapter: (deps && deps.adapter) || adapter,
    httpAPICall: function(url, options) {
      var fetchOptions = {
        method: options.verb,
        headers: {
          'Accept': 'application/json, text/plain, */*'
        },
        cache: 'no-cache'
      }
      if(options.verb === 'POST') {
        fetchOptions.headers['Content-Type'] = 'application/json'
      }
      if(options.withCredentials !== undefined) {
        fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit')
      }
      if(options.body) {
        fetchOptions.body = JSON.stringify(options.body)
      }

      var fetching = f(url, fetchOptions).catch(function(error) {
        return p.reject({message: 'Probably a network error, is the server down?', error: error})
      })

      /*
       * fetch() does not natively support timeouts.
       * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first.
       */

      if(options.timeout) {
        var timeout = new p(function(resolve, reject) {
          var timerId = setTimeout(function() {
            clearTimeout(timerId)
            return reject({message: 'Request timed out', timeout: options.timeout})
          }, options.timeout)
        })
        fetching = p.race([fetching, timeout])
      }

      fetching.then(function(response) {
        if(response.ok) {
          if(typeof(options.success) === typeof(Janus.noop)) {
            return response.json().then(function(parsed) {
              try {
                options.success(parsed)
              } catch(error) {
                Janus.error('Unhandled httpAPICall success callback error', error)
              }
            }, function(error) {
              return p.reject({message: 'Failed to parse response body', error: error, response: response})
            })
          }
        }
        else {
          return p.reject({message: 'API call failed', response: response})
        }
      }).catch(function(error) {
        if(typeof(options.error) === typeof(Janus.noop)) {
          options.error(error.message || '<< internal error >>', error)
        }
      })

      return fetching
    }
  }
}

Janus.useOldDependencies = function (deps) {
  var jq = (deps && deps.jQuery) || jQuery // eslint-disable-line no-undef
  var socketCls = (deps && deps.WebSocket) || WebSocket
  return {
    newWebSocket: function(server, proto) { return new socketCls(server, proto) },
    isArray: function(arr) { return jq.isArray(arr) },
    extension: (deps && deps.extension) || defaultExtension,
    webRTCAdapter: (deps && deps.adapter) || adapter,
    httpAPICall: function(url, options) {
      var payload = options.body !== undefined ? {
        contentType: 'application/json',
        data: JSON.stringify(options.body)
      } : {}
      var credentials = options.withCredentials !== undefined ? {xhrFields: {withCredentials: options.withCredentials}} : {}

      return jq.ajax(jq.extend(payload, credentials, {
        url: url,
        type: options.verb,
        cache: false,
        dataType: 'json',
        async: options.async,
        timeout: options.timeout,
        success: function(result) {
          if(typeof(options.success) === typeof(Janus.noop)) {
            options.success(result)
          }
        },
        error: function(xhr, status, err) {
          if(typeof(options.error) === typeof(Janus.noop)) {
            options.error(status, err)
          }
        }
      }))
    }
  }
}

Janus.noop = function() {}

Janus.dataChanDefaultLabel = 'JanusDataChannel'

// Note: in the future we may want to change this, e.g., as was
// attempted in https://github.com/meetecho/janus-gateway/issues/1670
Janus.endOfCandidates = null

// Stop all tracks from a given stream
Janus.stopAllTracks = function(stream) {
  try {
    // Try a MediaStreamTrack.stop() for each track
    var tracks = stream.getTracks()
    for(var mst of tracks) {
      Janus.log(mst)
      if(mst) {
        mst.stop()
      }
    }
  } catch(e) {
    // Do nothing if this fails
  }
}

// Initialization
Janus.init = function(options) {
  options = options || {}
  options.callback = (typeof options.callback == 'function') ? options.callback : Janus.noop
  if(Janus.initDone) {
    // Already initialized
    options.callback()
  } else {
    if(typeof console == 'undefined' || typeof console.log == 'undefined') {
      console = { log: function() {} }
    }
    // Console logging (all debugging disabled by default)
    Janus.trace = Janus.noop
    Janus.debug = Janus.noop
    Janus.vdebug = Janus.noop
    Janus.log = Janus.noop
    Janus.warn = Janus.noop
    Janus.error = Janus.noop
    if(options.debug === true || options.debug === 'all') {
      // Enable all debugging levels
      Janus.trace = console.trace.bind(console)
      Janus.debug = console.debug.bind(console)
      Janus.vdebug = console.debug.bind(console)
      Janus.log = console.log.bind(console)
      Janus.warn = console.warn.bind(console)
      Janus.error = console.error.bind(console)
    } else if(Array.isArray(options.debug)) {
      for(var d of options.debug) {
        switch(d) {
        case 'trace':
          Janus.trace = console.trace.bind(console)
          break
        case 'debug':
          Janus.debug = console.debug.bind(console)
          break
        case 'vdebug':
          Janus.vdebug = console.debug.bind(console)
          break
        case 'log':
          Janus.log = console.log.bind(console)
          break
        case 'warn':
          Janus.warn = console.warn.bind(console)
          break
        case 'error':
          Janus.error = console.error.bind(console)
          break
        default:
          console.error('Unknown debugging option \'' + d + '\' (supported: \'trace\', \'debug\', \'vdebug\', \'log\', warn\', \'error\')')
          break
        }
      }
    }
    Janus.log('Initializing library')

    var usedDependencies = options.dependencies || Janus.useDefaultDependencies()
    Janus.isArray = usedDependencies.isArray
    Janus.webRTCAdapter = usedDependencies.webRTCAdapter
    Janus.httpAPICall = usedDependencies.httpAPICall
    Janus.newWebSocket = usedDependencies.newWebSocket
    Janus.extension = usedDependencies.extension
    Janus.extension.init()

    // Helper method to enumerate devices
    Janus.listDevices = function(callback, config) {
      callback = (typeof callback == 'function') ? callback : Janus.noop
      if (config == null) config = { audio: true, video: true }
      if(Janus.isGetUserMediaAvailable()) {
        navigator.mediaDevices.getUserMedia(config)
          .then(function(stream) {
            navigator.mediaDevices.enumerateDevices().then(function(devices) {
              Janus.debug(devices)
              callback(devices)
              // Get rid of the now useless stream
              Janus.stopAllTracks(stream)
            })
          })
          .catch(function(err) {
            Janus.error(err)
            callback([])
          })
      } else {
        Janus.warn('navigator.mediaDevices unavailable')
        callback([])
      }
    }
    // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js)
    Janus.attachMediaStream = function(element, stream) {
      try {
        element.srcObject = stream
      } catch (e) {
        try {
          element.src = URL.createObjectURL(stream)
        } catch (e) {
          Janus.error('Error attaching stream to element')
        }
      }
    }
    Janus.reattachMediaStream = function(to, from) {
      try {
        to.srcObject = from.srcObject
      } catch (e) {
        try {
          to.src = from.src
        } catch (e) {
          Janus.error('Error reattaching stream to element')
        }
      }
    }
    // Detect tab close: make sure we don't loose existing onbeforeunload handlers
    // (note: for iOS we need to subscribe to a different event, 'pagehide', see
    // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe)
    var iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0
    var eventName = iOS ? 'pagehide' : 'beforeunload'
    var oldOBF = window['on' + eventName]
    window.addEventListener(eventName, function(event) {
      Janus.log('Closing window')
      for(var s in Janus.sessions) {
        if(Janus.sessions[s] && Janus.sessions[s].destroyOnUnload) {
          Janus.log('Destroying session ' + s)
          Janus.sessions[s].destroy({unload: true, notifyDestroyed: false})
        }
      }
      if(oldOBF && typeof oldOBF == 'function') {
        oldOBF()
      }
    })
    // If this is a Safari Technology Preview, check if VP8 is supported
    Janus.safariVp8 = false
    if(Janus.webRTCAdapter.browserDetails.browser === 'safari' &&
        Janus.webRTCAdapter.browserDetails.version >= 605) {
      // Let's see if RTCRtpSender.getCapabilities() is there
      if(RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities('video') &&
          RTCRtpSender.getCapabilities('video').codecs && RTCRtpSender.getCapabilities('video').codecs.length) {
        for(var codec of RTCRtpSender.getCapabilities('video').codecs) {
          if(codec && codec.mimeType && codec.mimeType.toLowerCase() === 'video/vp8') {
            Janus.safariVp8 = true
            break
          }
        }
        if(Janus.safariVp8) {
          Janus.log('This version of Safari supports VP8')
        } else {
          Janus.warn('This version of Safari does NOT support VP8: if you\'re using a Technology Preview, ' +
            'try enabling the \'WebRTC VP8 codec\' setting in the \'Experimental Features\' Develop menu')
        }
      } else {
        // We do it in a very ugly way, as there's no alternative...
        // We create a PeerConnection to see if VP8 is in an offer
        var testpc = new RTCPeerConnection({})
        testpc.createOffer({offerToReceiveVideo: true}).then(function(offer) {
          Janus.safariVp8 = offer.sdp.indexOf('VP8') !== -1
          if(Janus.safariVp8) {
            Janus.log('This version of Safari supports VP8')
          } else {
            Janus.warn('This version of Safari does NOT support VP8: if you\'re using a Technology Preview, ' +
              'try enabling the \'WebRTC VP8 codec\' setting in the \'Experimental Features\' Develop menu')
          }
          testpc.close()
          testpc = null
        })
      }
    }
    // Check if this browser supports Unified Plan and transceivers
    // Based on https://codepen.io/anon/pen/ZqLwWV?editors=0010
    Janus.unifiedPlan = false
    if(Janus.webRTCAdapter.browserDetails.browser === 'firefox' &&
        Janus.webRTCAdapter.browserDetails.version >= 59) {
      // Firefox definitely does, starting from version 59
      Janus.unifiedPlan = true
    } else if(Janus.webRTCAdapter.browserDetails.browser === 'chrome' &&
        Janus.webRTCAdapter.browserDetails.version >= 72) {
      // Chrome does, but it's only usable from version 72 on
      Janus.unifiedPlan = true
    } else if(!window.RTCRtpTransceiver || !('currentDirection' in RTCRtpTransceiver.prototype)) { // eslint-disable-line no-undef
      // Safari supports addTransceiver() but not Unified Plan when
      // currentDirection is not defined (see codepen above).
      Janus.unifiedPlan = false
    } else {
      // Check if addTransceiver() throws an exception
      var tempPc = new RTCPeerConnection()
      try {
        tempPc.addTransceiver('audio')
        Janus.unifiedPlan = true
      } catch (e) {}
      tempPc.close()
    }
    Janus.initDone = true
    options.callback()
  }
}

// Helper method to check whether WebRTC is supported by this browser
Janus.isWebrtcSupported = function() {
  return !!window.RTCPeerConnection
}
// Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP)
Janus.isGetUserMediaAvailable = function() {
  return navigator.mediaDevices && navigator.mediaDevices.getUserMedia
}

// Helper method to create random identifiers (e.g., transaction)
Janus.randomString = function(len) {
  var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  var randomString = ''
  for (var i = 0; i < len; i++) {
    var randomPoz = Math.floor(Math.random() * charSet.length)
    randomString += charSet.substring(randomPoz,randomPoz+1)
  }
  return randomString
}

function Janus(gatewayCallbacks) {
  gatewayCallbacks = gatewayCallbacks || {}
  gatewayCallbacks.success = (typeof gatewayCallbacks.success == 'function') ? gatewayCallbacks.success : Janus.noop
  gatewayCallbacks.error = (typeof gatewayCallbacks.error == 'function') ? gatewayCallbacks.error : Janus.noop
  gatewayCallbacks.wsopened = (typeof gatewayCallbacks.wsopened == 'function') ? gatewayCallbacks.wsopened : Janus.noop
  gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == 'function') ? gatewayCallbacks.destroyed : Janus.noop
  if(!Janus.initDone) {
    gatewayCallbacks.error('Library not initialized')
    return {}
  }
  if(!Janus.isWebrtcSupported()) {
    gatewayCallbacks.error('WebRTC not supported by this browser')
    return {}
  }
  Janus.log('Library initialized: ' + Janus.initDone)
  if(!gatewayCallbacks.server) {
    gatewayCallbacks.error('Invalid server url')
    return {}
  }
  var websockets = false
  var ws = null
  var wsHandlers = {}
  var wsKeepaliveTimeoutId = null
  var servers = null
  var serversIndex = 0
  var server = gatewayCallbacks.server
  if(Janus.isArray(server)) {
    Janus.log('Multiple servers provided (' + server.length + '), will use the first that works')
    server = null
    servers = gatewayCallbacks.server
    Janus.debug(servers)
  } else {
    if(server.indexOf('ws') === 0) {
      websockets = true
      Janus.log('Using WebSockets to contact Janus: ' + server)
    } else {
      websockets = false
      Janus.log('Using REST API to contact Janus: ' + server)
    }
  }
  var iceServers = gatewayCallbacks.iceServers || [{urls: 'stun:stun.l.google.com:19302'}]
  var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy
  var bundlePolicy = gatewayCallbacks.bundlePolicy
  // Whether IPv6 candidates should be gathered
  var ipv6Support = (gatewayCallbacks.ipv6 === true)
  // Whether we should enable the withCredentials flag for XHR requests
  var withCredentials = false
  if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null)
    withCredentials = gatewayCallbacks.withCredentials === true
  // Optional max events
  var maxev = 10
  if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null)
    maxev = gatewayCallbacks.max_poll_events
  if(maxev < 1)
    maxev = 1
  // Token to use (only if the token based authentication mechanism is enabled)
  var token = null
  if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null)
    token = gatewayCallbacks.token
  // API secret to use (only if the shared API secret is enabled)
  var apisecret = null
  if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null)
    apisecret = gatewayCallbacks.apisecret
  // Whether we should destroy this session when onbeforeunload is called
  this.destroyOnUnload = true
  if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null)
    this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true)
  // Some timeout-related values
  var keepAlivePeriod = 25000
  if(gatewayCallbacks.keepAlivePeriod !== undefined && gatewayCallbacks.keepAlivePeriod !== null)
    keepAlivePeriod = gatewayCallbacks.keepAlivePeriod
  if(isNaN(keepAlivePeriod))
    keepAlivePeriod = 25000
  var longPollTimeout = 60000
  if(gatewayCallbacks.longPollTimeout !== undefined && gatewayCallbacks.longPollTimeout !== null)
    longPollTimeout = gatewayCallbacks.longPollTimeout
  if(isNaN(longPollTimeout))
    longPollTimeout = 60000

  // overrides for default maxBitrate values for simulcasting
  function getMaxBitrates(simulcastMaxBitrates) {
    var maxBitrates = {
      high: 900000,
      medium: 300000,
      low: 100000,
    }

    if (simulcastMaxBitrates !== undefined && simulcastMaxBitrates !== null) {
      if (simulcastMaxBitrates.high)
        maxBitrates.high = simulcastMaxBitrates.high
      if (simulcastMaxBitrates.medium)
        maxBitrates.medium = simulcastMaxBitrates.medium
      if (simulcastMaxBitrates.low)
        maxBitrates.low = simulcastMaxBitrates.low
    }

    return maxBitrates
  }

  var connected = false
  var sessionId = null
  var pluginHandles = {}
  var that = this
  var retries = 0
  var transactions = {}
  createSession(gatewayCallbacks)

  // Public methods
  this.getServer = function() { return server }
  this.isConnected = function() { return connected }
  this.reconnect = function(callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    callbacks['reconnect'] = true
    createSession(callbacks)
  }
  this.getSessionId = function() { return sessionId }
  this.getInfo = function(callbacks) { getInfo(callbacks) }
  this.destroy = function(callbacks) { destroySession(callbacks) }
  this.attach = function(callbacks) { createHandle(callbacks) }

  function eventHandler() {
    if(sessionId == null)
      return
    Janus.debug('Long poll...')
    if(!connected) {
      Janus.warn('Is the server down? (connected=false)')
      return
    }
    var longpoll = server + '/' + sessionId + '?rid=' + new Date().getTime()
    if(maxev)
      longpoll = longpoll + '&maxev=' + maxev
    if(token)
      longpoll = longpoll + '&token=' + encodeURIComponent(token)
    if(apisecret)
      longpoll = longpoll + '&apisecret=' + encodeURIComponent(apisecret)
    Janus.httpAPICall(longpoll, {
      verb: 'GET',
      withCredentials: withCredentials,
      success: handleEvent,
      timeout: longPollTimeout,
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown)
        retries++
        if(retries > 3) {
          // Did we just lose the server? :-(
          connected = false
          gatewayCallbacks.error('Lost connection to the server (is it down?)')
          return
        }
        eventHandler()
      }
    })
  }

  // Private event handler: this will trigger plugin callbacks, if set
  function handleEvent(json, skipTimeout) {
    retries = 0
    if(!websockets && sessionId !== undefined && sessionId !== null && skipTimeout !== true)
      eventHandler()
    if(!websockets && Janus.isArray(json)) {
      // We got an array: it means we passed a maxev > 1, iterate on all objects
      for(var i=0; i<json.length; i++) {
        handleEvent(json[i], true)
      }
      return
    }

    if (json.session_id && json.session_id !== sessionId) {
      return
    }

    if(json['janus'] === 'keepalive') {
      // Nothing happened
      Janus.vdebug('Got a keepalive on session ' + sessionId)
      return
    } else if(json['janus'] === 'server_info') {
      // Just info on the Janus instance
      Janus.debug('Got info on the Janus instance')
      Janus.debug(json)
      var transaction = json['transaction']
      if(transaction) {
        var reportSuccess = transactions[transaction]
        if(reportSuccess)
          reportSuccess(json)
        delete transactions[transaction]
      }
      return
    } else if(json['janus'] === 'ack') {
      // Just an ack, we can probably ignore
      Janus.debug('Got an ack on session ' + sessionId)
      Janus.debug(json)
      var transaction = json['transaction']
      if(transaction) {
        var reportSuccess = transactions[transaction]
        if(reportSuccess)
          reportSuccess(json)
        delete transactions[transaction]
      }
      return
    } else if(json['janus'] === 'success') {
      // Success!
      Janus.debug('Got a success on session ' + sessionId)
      Janus.debug(json)
      var transaction = json['transaction']
      if(transaction) {
        var reportSuccess = transactions[transaction]
        if(reportSuccess)
          reportSuccess(json)
        delete transactions[transaction]
      }
      return
    } else if(json['janus'] === 'trickle') {
      // We got a trickle candidate from Janus
      var sender = json['sender']
      if(!sender) {
        Janus.warn('Missing sender...')
        return
      }
      var pluginHandle = pluginHandles[sender]
      if(!pluginHandle) {
        Janus.debug('This handle is not attached to this session')
        return
      }
      var candidate = json['candidate']
      Janus.debug('Got a trickled candidate on session ' + sessionId)
      Janus.debug(candidate)
      var config = pluginHandle.webrtcStuff
      if(config.pc && config.remoteSdp) {
        // Add candidate right now
        Janus.debug('Adding remote candidate:', candidate)
        if(!candidate || candidate.completed === true) {
          // end-of-candidates
          config.pc.addIceCandidate(Janus.endOfCandidates)
        } else {
          // New candidate
          config.pc.addIceCandidate(candidate)
        }
      } else {
        // We didn't do setRemoteDescription (trickle got here before the offer?)
        Janus.debug('We didn\'t do setRemoteDescription (trickle got here before the offer?), caching candidate')
        if(!config.candidates)
          config.candidates = []
        config.candidates.push(candidate)
        Janus.debug(config.candidates)
      }
    } else if(json['janus'] === 'webrtcup') {
      // The PeerConnection with the server is up! Notify this
      Janus.debug('Got a webrtcup event on session ' + sessionId)
      Janus.debug(json)
      var sender = json['sender']
      if(!sender) {
        Janus.warn('Missing sender...')
        return
      }
      var pluginHandle = pluginHandles[sender]
      if(!pluginHandle) {
        Janus.debug('This handle is not attached to this session')
        return
      }
      pluginHandle.webrtcState(true)
      return
    } else if(json['janus'] === 'hangup') {
      // A plugin asked the core to hangup a PeerConnection on one of our handles
      Janus.debug('Got a hangup event on session ' + sessionId)
      Janus.debug(json)
      var sender = json['sender']
      if(!sender) {
        Janus.warn('Missing sender...')
        return
      }
      var pluginHandle = pluginHandles[sender]
      if(!pluginHandle) {
        Janus.debug('This handle is not attached to this session')
        return
      }
      pluginHandle.webrtcState(false, json['reason'])
      pluginHandle.hangup()
    } else if(json['janus'] === 'detached') {
      // A plugin asked the core to detach one of our handles
      Janus.debug('Got a detached event on session ' + sessionId)
      Janus.debug(json)
      var sender = json['sender']
      if(!sender) {
        Janus.warn('Missing sender...')
        return
      }
      var pluginHandle = pluginHandles[sender]
      if(!pluginHandle) {
        // Don't warn here because destroyHandle causes this situation.
        return
      }

      // copied from our prev custom release, possibly a bug fix, maybe no longer necessary.
      pluginHandle.detached = true

      pluginHandle.ondetached()
      pluginHandle.detach()
    } else if(json['janus'] === 'media') {
      // Media started/stopped flowing
      Janus.debug('Got a media event on session ' + sessionId)
      Janus.debug(json)
      var sender = json['sender']
      if(!sender) {
        Janus.warn('Missing sender...')
        return
      }
      var pluginHandle = pluginHandles[sender]
      if(!pluginHandle) {
        Janus.debug('This handle is not attached to this session')
        return
      }
      pluginHandle.mediaState(json['type'], json['receiving'])
    } else if(json['janus'] === 'slowlink') {
      Janus.debug('Got a slowlink event on session ' + sessionId)
      Janus.debug(json)
      // Trouble uplink or downlink
      var sender = json['sender']
      if(!sender) {
        Janus.warn('Missing sender...')
        return
      }
      var pluginHandle = pluginHandles[sender]
      if(!pluginHandle) {
        Janus.debug('This handle is not attached to this session')
        return
      }
      pluginHandle.slowLink(json['uplink'], json['lost'])
    } else if(json['janus'] === 'error') {
      // Oops, something wrong happened
      Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
      Janus.debug(json)
      var transaction = json['transaction']
      if(transaction) {
        var reportSuccess = transactions[transaction]
        if(reportSuccess) {
          reportSuccess(json)
        }
        delete transactions[transaction]
      }

      if (json.error.code === 458) {
        if (websockets) {
          ws.close(3505, 'No such session')
        }
      }

      return
    } else if(json['janus'] === 'event') {
      Janus.debug('Got a plugin event on session ' + sessionId)
      Janus.debug(json)
      var sender = json['sender']
      if(!sender) {
        Janus.warn('Missing sender...')
        return
      }
      var plugindata = json['plugindata']
      if(!plugindata) {
        Janus.warn('Missing plugindata...')
        return
      }
      Janus.debug('  -- Event is coming from ' + sender + ' (' + plugindata['plugin'] + ')')
      var data = plugindata['data']
      Janus.debug(data)
      var pluginHandle = pluginHandles[sender]
      if(!pluginHandle) {
        Janus.warn('This handle is not attached to this session')
        return
      }
      var jsep = json['jsep']
      if(jsep) {
        Janus.debug('Handling SDP as well...')
        Janus.debug(jsep)
      }
      var callback = pluginHandle.onmessage
      if(callback) {
        Janus.debug('Notifying application...')
        // Send to callback specified when attaching plugin handle
        callback(data, jsep)
      } else {
        // Send to generic callback (?)
        Janus.debug('No provided notification callback')
      }
    } else if(json['janus'] === 'timeout') {
      Janus.error('Timeout on session ' + sessionId)
      Janus.debug(json)
      if (websockets) {
        ws.close(3504, 'Gateway timeout')
      }
      return
    } else {
      Janus.warn('Unknown message/event  \'' + json['janus'] + '\' on session ' + sessionId)
      Janus.debug(json)
    }
  }

  // Private helper to send keep-alive messages on WebSockets
  function keepAlive() {
    if(!server || !websockets || !connected)
      return
    wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod)
    var request = { 'janus': 'keepalive', 'session_id': sessionId, 'transaction': Janus.randomString(12) }
    if(token)
      request['token'] = token
    if(apisecret)
      request['apisecret'] = apisecret
    ws.send(JSON.stringify(request))
  }

  // Private method to create a session
  function createSession(callbacks) {
    var transaction = Janus.randomString(12)
    var request = { 'janus': 'create', 'transaction': transaction }
    if(callbacks['reconnect']) {
      // We're reconnecting, claim the session
      connected = false
      request['janus'] = 'claim'
      request['session_id'] = sessionId
      // If we were using websockets, ignore the old connection
      if(ws) {
        ws.onopen = null
        ws.onerror = null
        ws.onclose = null
        if(wsKeepaliveTimeoutId) {
          clearTimeout(wsKeepaliveTimeoutId)
          wsKeepaliveTimeoutId = null
        }
      }
    }
    if(token)
      request['token'] = token
    if(apisecret)
      request['apisecret'] = apisecret
    if(!server && Janus.isArray(servers)) {
      // We still need to find a working server from the list we were given
      server = servers[serversIndex]
      if(server.indexOf('ws') === 0) {
        websockets = true
        Janus.log('Server #' + (serversIndex+1) + ': trying WebSockets to contact Janus (' + server + ')')
      } else {
        websockets = false
        Janus.log('Server #' + (serversIndex+1) + ': trying REST API to contact Janus (' + server + ')')
      }
    }
    if(websockets) {
      ws = Janus.newWebSocket(server, 'janus-protocol')
      wsHandlers = {
        'error': function() {
          Janus.error('Error connecting to the Janus WebSockets server... ' + server)
          if (Janus.isArray(servers) && !callbacks['reconnect']) {
            serversIndex++
            if (serversIndex === servers.length) {
              // We tried all the servers the user gave us and they all failed
              callbacks.error('Error connecting to any of the provided Janus servers: Is the server down?')
              return
            }
            // Let's try the next server
            server = null
            setTimeout(function() {
              createSession(callbacks)
            }, 200)
            return
          }
          callbacks.error('Error connecting to the Janus WebSockets server: Is the server down?')
        },

        'open': function() {
          callbacks.wsopened()

          // We need to be notified about the success
          transactions[transaction] = function(json) {
            Janus.debug(json)
            if (json['janus'] !== 'success') {
              Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
              callbacks.error(json['error'].reason)
              return
            }
            wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod)
            connected = true
            sessionId = json['session_id'] ? json['session_id'] : json.data['id']
            if(callbacks['reconnect']) {
              Janus.log('Claimed session: ' + sessionId)
            } else {
              Janus.log('Created session: ' + sessionId)
            }
            Janus.sessions[sessionId] = that
            callbacks.success()
          }
          ws.send(JSON.stringify(request))
        },

        'message': function(event) {
          handleEvent(JSON.parse(event.data))
        },

        'close': function() {
          if (!server || !connected) {
            return
          }
          connected = false
          // FIXME What if this is called when the page is closed?
          gatewayCallbacks.error('Lost connection to the server (is it down?)')
        }
      }

      for(var eventName in wsHandlers) {
        ws.addEventListener(eventName, wsHandlers[eventName])
      }

      return
    }
    Janus.httpAPICall(server, {
      verb: 'POST',
      withCredentials: withCredentials,
      body: request,
      success: function(json) {
        Janus.debug(json)
        if(json['janus'] !== 'success') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
          callbacks.error(json['error'].reason)
          return
        }
        connected = true
        sessionId = json['session_id'] ? json['session_id'] : json.data['id']
        if(callbacks['reconnect']) {
          Janus.log('Claimed session: ' + sessionId)
        } else {
          Janus.log('Created session: ' + sessionId)
        }
        Janus.sessions[sessionId] = that
        eventHandler()
        callbacks.success()
      },
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown) // FIXME
        if(Janus.isArray(servers) && !callbacks['reconnect']) {
          serversIndex++
          if(serversIndex === servers.length) {
            // We tried all the servers the user gave us and they all failed
            callbacks.error('Error connecting to any of the provided Janus servers: Is the server down?')
            return
          }
          // Let's try the next server
          server = null
          setTimeout(function() { createSession(callbacks) }, 200)
          return
        }
        if(errorThrown === '')
          callbacks.error(textStatus + ': Is the server down?')
        else
          callbacks.error(textStatus + ': ' + errorThrown)
      }
    })
  }

  // Private method to get info on the server
  function getInfo(callbacks) {
    callbacks = callbacks || {}
    // FIXME This method triggers a success even when we fail
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    Janus.log('Getting info on Janus instance')
    if(!connected) {
      Janus.warn('Is the server down? (connected=false)')
      callbacks.error('Is the server down? (connected=false)')
      return
    }
    // We just need to send an "info" request
    var transaction = Janus.randomString(12)
    var request = { 'janus': 'info', 'transaction': transaction }
    if(token)
      request['token'] = token
    if(apisecret)
      request['apisecret'] = apisecret
    if(websockets) {
      transactions[transaction] = function(json) {
        Janus.log('Server info:')
        Janus.debug(json)
        if(json['janus'] !== 'server_info') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
        }
        callbacks.success(json)
      }
      ws.send(JSON.stringify(request))
      return
    }
    Janus.httpAPICall(server, {
      verb: 'POST',
      withCredentials: withCredentials,
      body: request,
      success: function(json) {
        Janus.log('Server info:')
        Janus.debug(json)
        if(json['janus'] !== 'server_info') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
        }
        callbacks.success(json)
      },
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown) // FIXME
        if(errorThrown === '')
          callbacks.error(textStatus + ': Is the server down?')
        else
          callbacks.error(textStatus + ': ' + errorThrown)
      }
    })
  }

  // Private method to destroy a session
  function destroySession(callbacks) {
    callbacks = callbacks || {}
    // FIXME This method triggers a success even when we fail
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    var unload = (callbacks.unload === true)
    var notifyDestroyed = true
    if(callbacks.notifyDestroyed !== undefined && callbacks.notifyDestroyed !== null)
      notifyDestroyed = (callbacks.notifyDestroyed === true)
    var cleanupHandles = (callbacks.cleanupHandles === true)
    Janus.log('Destroying session ' + sessionId + ' (unload=' + unload + ')')
    if(!sessionId) {
      Janus.warn('No session to destroy')
      callbacks.success()
      if(notifyDestroyed)
        gatewayCallbacks.destroyed()
      return
    }
    if(cleanupHandles) {
      for(var handleId in pluginHandles)
        destroyHandle(handleId, { noRequest: true })
    }
    if(!connected) {
      Janus.warn('Is the server down? (connected=false)')
      sessionId = null
      callbacks.success()
      return
    }
    // No need to destroy all handles first, Janus will do that itself
    var request = { 'janus': 'destroy', 'transaction': Janus.randomString(12) }
    if(token)
      request['token'] = token
    if(apisecret)
      request['apisecret'] = apisecret
    if(unload) {
      // We're unloading the page: use sendBeacon for HTTP instead,
      // or just close the WebSocket connection if we're using that
      if(websockets) {
        ws.onclose = null
        ws.close()
        ws = null
      } else {
        navigator.sendBeacon(server + '/' + sessionId, JSON.stringify(request))
      }
      Janus.log('Destroyed session:')
      sessionId = null
      connected = false
      callbacks.success()
      if(notifyDestroyed)
        gatewayCallbacks.destroyed()
      return
    }
    if(websockets) {
      request['session_id'] = sessionId

      var unbindWebSocket = function() {
        for(var eventName in wsHandlers) {
          ws.removeEventListener(eventName, wsHandlers[eventName])
        }
        ws.removeEventListener('message', onUnbindMessage)
        ws.removeEventListener('error', onUnbindError)
        if(wsKeepaliveTimeoutId) {
          clearTimeout(wsKeepaliveTimeoutId)
        }
        ws.close()
      }

      var onUnbindMessage = function(event){
        var data = JSON.parse(event.data)
        if(data.session_id == request.session_id && data.transaction == request.transaction) {
          unbindWebSocket()
          callbacks.success()
          if(notifyDestroyed)
            gatewayCallbacks.destroyed()
        }
      }
      var onUnbindError = function(event) {
        unbindWebSocket()
        callbacks.error('Failed to destroy the server: Is the server down?')
        if(notifyDestroyed)
          gatewayCallbacks.destroyed()
      }

      ws.addEventListener('message', onUnbindMessage)
      ws.addEventListener('error', onUnbindError)

      if (ws.readyState === 1) {
        ws.send(JSON.stringify(request))
      } else {
        onUnbindError()
      }

      return
    }
    Janus.httpAPICall(server + '/' + sessionId, {
      verb: 'POST',
      withCredentials: withCredentials,
      body: request,
      success: function(json) {
        Janus.log('Destroyed session:')
        Janus.debug(json)
        sessionId = null
        connected = false
        if(json['janus'] !== 'success') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
        }
        callbacks.success()
        if(notifyDestroyed)
          gatewayCallbacks.destroyed()
      },
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown) // FIXME
        // Reset everything anyway
        sessionId = null
        connected = false
        callbacks.success()
        if(notifyDestroyed)
          gatewayCallbacks.destroyed()
      }
    })
  }

  // Private method to create a plugin handle
  function createHandle(callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    callbacks.consentDialog = (typeof callbacks.consentDialog == 'function') ? callbacks.consentDialog : Janus.noop
    callbacks.iceState = (typeof callbacks.iceState == 'function') ? callbacks.iceState : Janus.noop
    callbacks.mediaState = (typeof callbacks.mediaState == 'function') ? callbacks.mediaState : Janus.noop
    callbacks.webrtcState = (typeof callbacks.webrtcState == 'function') ? callbacks.webrtcState : Janus.noop
    callbacks.slowLink = (typeof callbacks.slowLink == 'function') ? callbacks.slowLink : Janus.noop
    callbacks.onmessage = (typeof callbacks.onmessage == 'function') ? callbacks.onmessage : Janus.noop
    callbacks.onlocalstream = (typeof callbacks.onlocalstream == 'function') ? callbacks.onlocalstream : Janus.noop
    callbacks.onremotestream = (typeof callbacks.onremotestream == 'function') ? callbacks.onremotestream : Janus.noop
    callbacks.ondata = (typeof callbacks.ondata == 'function') ? callbacks.ondata : Janus.noop
    callbacks.ondataopen = (typeof callbacks.ondataopen == 'function') ? callbacks.ondataopen : Janus.noop
    callbacks.oncleanup = (typeof callbacks.oncleanup == 'function') ? callbacks.oncleanup : Janus.noop
    callbacks.ondetached = (typeof callbacks.ondetached == 'function') ? callbacks.ondetached : Janus.noop
    if(!connected) {
      Janus.warn('Is the server down? (connected=false)')
      callbacks.error('Is the server down? (connected=false)')
      return
    }
    var plugin = callbacks.plugin
    if(!plugin) {
      Janus.error('Invalid plugin')
      callbacks.error('Invalid plugin')
      return
    }
    var opaqueId = callbacks.opaqueId
    var handleToken = callbacks.token ? callbacks.token : token
    var transaction = Janus.randomString(12)
    var request = { 'janus': 'attach', 'plugin': plugin, 'opaque_id': opaqueId, 'transaction': transaction }
    if(handleToken)
      request['token'] = handleToken
    if(apisecret)
      request['apisecret'] = apisecret
    if(websockets) {
      transactions[transaction] = function(json) {
        Janus.debug(json)
        if(json['janus'] !== 'success') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
          callbacks.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason)
          return
        }
        var handleId = json.data['id']
        Janus.log('Created handle: ' + handleId)
        var pluginHandle =
          {
            session : that,
            plugin : plugin,
            id : handleId,
            token : handleToken,
            detached : false,
            webrtcStuff : {
              started : false,
              myStream : null,
              streamExternal : false,
              remoteStream : null,
              mySdp : null,
              mediaConstraints : null,
              pc : null,
              dataChannel : {},
              dtmfSender : null,
              trickle : true,
              iceDone : false,
              volume : {
                value : null,
                timer : null
              },
              bitrate : {
                value : null,
                bsnow : null,
                bsbefore : null,
                tsnow : null,
                tsbefore : null,
                timer : null
              }
            },
            getId : function() { return handleId },
            getPlugin : function() { return plugin },
            getVolume : function() { return getVolume(handleId, true) },
            getRemoteVolume : function() { return getVolume(handleId, true) },
            getLocalVolume : function() { return getVolume(handleId, false) },
            isAudioMuted : function() { return isMuted(handleId, false) },
            muteAudio : function() { return mute(handleId, false, true) },
            unmuteAudio : function() { return mute(handleId, false, false) },
            isVideoMuted : function() { return isMuted(handleId, true) },
            muteVideo : function() { return mute(handleId, true, true) },
            unmuteVideo : function() { return mute(handleId, true, false) },
            getBitrate : function() { return getBitrate(handleId) },
            send : function(callbacks) { sendMessage(handleId, callbacks) },
            data : function(callbacks) { sendData(handleId, callbacks) },
            dtmf : function(callbacks) { sendDtmf(handleId, callbacks) },
            consentDialog : callbacks.consentDialog,
            iceState : callbacks.iceState,
            mediaState : callbacks.mediaState,
            webrtcState : callbacks.webrtcState,
            slowLink : callbacks.slowLink,
            onmessage : callbacks.onmessage,
            createOffer : function(callbacks) { prepareWebrtc(handleId, true, callbacks) },
            createAnswer : function(callbacks) { prepareWebrtc(handleId, false, callbacks) },
            handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks) },
            onlocalstream : callbacks.onlocalstream,
            onremotestream : callbacks.onremotestream,
            ontrackmuted : callbacks.ontrackmuted,
            ontrackunmuted : callbacks.ontrackunmuted,
            ondata : callbacks.ondata,
            ondataopen : callbacks.ondataopen,
            oncleanup : callbacks.oncleanup,
            ondetached : callbacks.ondetached,
            hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true) },
            detach : function(callbacks) { destroyHandle(handleId, callbacks) }
          }
        pluginHandles[handleId] = pluginHandle
        callbacks.success(pluginHandle)
      }
      request['session_id'] = sessionId
      ws.send(JSON.stringify(request))
      return
    }
    Janus.httpAPICall(server + '/' + sessionId, {
      verb: 'POST',
      withCredentials: withCredentials,
      body: request,
      success: function(json) {
        Janus.debug(json)
        if(json['janus'] !== 'success') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
          callbacks.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason)
          return
        }
        var handleId = json.data['id']
        Janus.log('Created handle: ' + handleId)
        var pluginHandle =
          {
            session : that,
            plugin : plugin,
            id : handleId,
            token : handleToken,
            detached : false,
            webrtcStuff : {
              started : false,
              myStream : null,
              streamExternal : false,
              remoteStream : null,
              mySdp : null,
              mediaConstraints : null,
              pc : null,
              dataChannel : {},
              dtmfSender : null,
              trickle : true,
              iceDone : false,
              volume : {
                value : null,
                timer : null
              },
              bitrate : {
                value : null,
                bsnow : null,
                bsbefore : null,
                tsnow : null,
                tsbefore : null,
                timer : null
              }
            },
            getId : function() { return handleId },
            getPlugin : function() { return plugin },
            getVolume : function() { return getVolume(handleId, true) },
            getRemoteVolume : function() { return getVolume(handleId, true) },
            getLocalVolume : function() { return getVolume(handleId, false) },
            isAudioMuted : function() { return isMuted(handleId, false) },
            muteAudio : function() { return mute(handleId, false, true) },
            unmuteAudio : function() { return mute(handleId, false, false) },
            isVideoMuted : function() { return isMuted(handleId, true) },
            muteVideo : function() { return mute(handleId, true, true) },
            unmuteVideo : function() { return mute(handleId, true, false) },
            getBitrate : function() { return getBitrate(handleId) },
            send : function(callbacks) { sendMessage(handleId, callbacks) },
            data : function(callbacks) { sendData(handleId, callbacks) },
            dtmf : function(callbacks) { sendDtmf(handleId, callbacks) },
            consentDialog : callbacks.consentDialog,
            iceState : callbacks.iceState,
            mediaState : callbacks.mediaState,
            webrtcState : callbacks.webrtcState,
            slowLink : callbacks.slowLink,
            onmessage : callbacks.onmessage,
            createOffer : function(callbacks) { prepareWebrtc(handleId, true, callbacks) },
            createAnswer : function(callbacks) { prepareWebrtc(handleId, false, callbacks) },
            handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks) },
            onlocalstream : callbacks.onlocalstream,
            onremotestream : callbacks.onremotestream,
            ontrackmuted : callbacks.ontrackmuted,
            ontrackunmuted : callbacks.ontrackunmuted,
            ondata : callbacks.ondata,
            ondataopen : callbacks.ondataopen,
            oncleanup : callbacks.oncleanup,
            ondetached : callbacks.ondetached,
            hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true) },
            detach : function(callbacks) { destroyHandle(handleId, callbacks) }
          }
        pluginHandles[handleId] = pluginHandle
        callbacks.success(pluginHandle)
      },
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown) // FIXME
        if(errorThrown === '')
          callbacks.error(textStatus + ': Is the server down?')
        else
          callbacks.error(textStatus + ': ' + errorThrown)
      }
    })
  }

  // Private method to send a message
  function sendMessage(handleId, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    if(!connected) {
      Janus.warn('Is the server down? (connected=false)')
      callbacks.error('Is the server down? (connected=false)')
      return
    }
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      callbacks.error('Invalid handle')
      return
    }
    var message = callbacks.message
    var jsep = callbacks.jsep
    var transaction = Janus.randomString(12)
    var request = { 'janus': 'message', 'body': message, 'transaction': transaction }
    if(pluginHandle.token)
      request['token'] = pluginHandle.token
    if(apisecret)
      request['apisecret'] = apisecret
    if(jsep) {
      request.jsep = {
        type: jsep.type,
        sdp: jsep.sdp
      }
      if(jsep.e2ee)
        request.jsep.e2ee = true
      if(jsep.rid_order === 'hml' || jsep.rid_order === 'lmh')
        request.jsep.rid_order = jsep.rid_order
    }
    Janus.debug('Sending message to plugin (handle=' + handleId + '):')
    Janus.debug(request)
    if(websockets) {
      request['session_id'] = sessionId
      request['handle_id'] = handleId
      transactions[transaction] = function(json) {
        Janus.debug('Message sent!')
        Janus.debug(json)
        if(json['janus'] === 'success') {
          // We got a success, must have been a synchronous transaction
          var plugindata = json['plugindata']
          if(!plugindata) {
            Janus.warn('Request succeeded, but missing plugindata...')
            callbacks.success()
            return
          }
          Janus.log('Synchronous transaction successful (' + plugindata['plugin'] + ')')
          var data = plugindata['data']
          Janus.debug(data)
          callbacks.success(data)
          return
        } else if(json['janus'] !== 'ack') {
          // Not a success and not an ack, must be an error
          if(json['error']) {
            Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
            callbacks.error(json['error'].code + ' ' + json['error'].reason)
          } else {
            Janus.error('Unknown error') // FIXME
            callbacks.error('Unknown error')
          }
          return
        }
        // If we got here, the plugin decided to handle the request asynchronously
        callbacks.success()
      }
      ws.send(JSON.stringify(request))
      return
    }
    Janus.httpAPICall(server + '/' + sessionId + '/' + handleId, {
      verb: 'POST',
      withCredentials: withCredentials,
      body: request,
      success: function(json) {
        Janus.debug('Message sent!')
        Janus.debug(json)
        if(json['janus'] === 'success') {
          // We got a success, must have been a synchronous transaction
          var plugindata = json['plugindata']
          if(!plugindata) {
            Janus.warn('Request succeeded, but missing plugindata...')
            callbacks.success()
            return
          }
          Janus.log('Synchronous transaction successful (' + plugindata['plugin'] + ')')
          var data = plugindata['data']
          Janus.debug(data)
          callbacks.success(data)
          return
        } else if(json['janus'] !== 'ack') {
          // Not a success and not an ack, must be an error
          if(json['error']) {
            Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
            callbacks.error(json['error'].code + ' ' + json['error'].reason)
          } else {
            Janus.error('Unknown error') // FIXME
            callbacks.error('Unknown error')
          }
          return
        }
        // If we got here, the plugin decided to handle the request asynchronously
        callbacks.success()
      },
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown) // FIXME
        callbacks.error(textStatus + ': ' + errorThrown)
      }
    })
  }

  // Private method to send a trickle candidate
  function sendTrickleCandidate(handleId, candidate) {
    if(!connected) {
      Janus.warn('Is the server down? (connected=false)')
      return
    }
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      return
    }
    var request = { 'janus': 'trickle', 'candidate': candidate, 'transaction': Janus.randomString(12) }
    if(pluginHandle.token)
      request['token'] = pluginHandle.token
    if(apisecret)
      request['apisecret'] = apisecret
    Janus.vdebug('Sending trickle candidate (handle=' + handleId + '):')
    Janus.vdebug(request)
    if(websockets) {
      request['session_id'] = sessionId
      request['handle_id'] = handleId
      ws.send(JSON.stringify(request))
      return
    }
    Janus.httpAPICall(server + '/' + sessionId + '/' + handleId, {
      verb: 'POST',
      withCredentials: withCredentials,
      body: request,
      success: function(json) {
        Janus.vdebug('Candidate sent!')
        Janus.vdebug(json)
        if(json['janus'] !== 'ack') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
          return
        }
      },
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown) // FIXME
      }
    })
  }

  // Private method to create a data channel
  function createDataChannel(handleId, dclabel, dcprotocol, incoming, pendingData) {
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    if(!config.pc) {
      Janus.warn('Invalid PeerConnection')
      return
    }
    var onDataChannelMessage = function(event) {
      var label = event.target.label
      pluginHandle.ondata(event.data, label)
    }
    var onDataChannelStateChange = function(event) {
      Janus.log('Received state change on data channel:', event)
      var label = event.target.label
      var protocol = event.target.protocol
      var dcState = config.dataChannel[label] ? config.dataChannel[label].readyState : 'null'
      Janus.log('State change on <' + label + '> data channel: ' + dcState)
      if(dcState === 'open') {
        // Any pending messages to send?
        if(config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) {
          Janus.log('Sending pending messages on <' + label + '>:', config.dataChannel[label].pending.length)
          for(var data of config.dataChannel[label].pending) {
            Janus.log('Sending data on data channel <' + label + '>')
            Janus.debug(data)
            config.dataChannel[label].send(data)
          }
          config.dataChannel[label].pending = []
        }
        // Notify the open data channel
        pluginHandle.ondataopen(label, protocol)
      }
    }
    var onDataChannelError = function(error) {
      Janus.error('Got error on data channel:', error)
      // TODO
    }
    if(!incoming) {
      // FIXME Add options (ordered, maxRetransmits, etc.)
      var dcoptions = { ordered: true }
      if(dcprotocol)
        dcoptions.protocol = dcprotocol
      config.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, dcoptions)
    } else {
      // The channel was created by Janus
      config.dataChannel[dclabel] = incoming
    }
    config.dataChannel[dclabel].onmessage = onDataChannelMessage
    config.dataChannel[dclabel].onopen = onDataChannelStateChange
    config.dataChannel[dclabel].onclose = onDataChannelStateChange
    config.dataChannel[dclabel].onerror = onDataChannelError
    config.dataChannel[dclabel].pending = []
    if(pendingData)
      config.dataChannel[dclabel].pending.push(pendingData)
  }

  // Private method to send a data channel message
  function sendData(handleId, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      callbacks.error('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    var data = callbacks.text || callbacks.data
    if(!data) {
      Janus.warn('Invalid data')
      callbacks.error('Invalid data')
      return
    }
    var label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel
    if(!config.dataChannel[label]) {
      // Create new data channel and wait for it to open
      createDataChannel(handleId, label, callbacks.protocol, false, data, callbacks.protocol)
      callbacks.success()
      return
    }
    if(config.dataChannel[label].readyState !== 'open') {
      config.dataChannel[label].pending.push(data)
      callbacks.success()
      return
    }
    Janus.log('Sending data on data channel <' + label + '>')
    Janus.debug(data)
    config.dataChannel[label].send(data)
    callbacks.success()
  }

  // Private method to send a DTMF tone
  function sendDtmf(handleId, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      callbacks.error('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    if(!config.dtmfSender) {
      // Create the DTMF sender the proper way, if possible
      if(config.pc) {
        var senders = config.pc.getSenders()
        var audioSender = senders.find(function(sender) {
          return sender.track && sender.track.kind === 'audio'
        })
        if(!audioSender) {
          Janus.warn('Invalid DTMF configuration (no audio track)')
          callbacks.error('Invalid DTMF configuration (no audio track)')
          return
        }
        config.dtmfSender = audioSender.dtmf
        if(config.dtmfSender) {
          Janus.log('Created DTMF Sender')
          config.dtmfSender.ontonechange = function(tone) { Janus.debug('Sent DTMF tone: ' + tone.tone) }
        }
      }
      if(!config.dtmfSender) {
        Janus.warn('Invalid DTMF configuration')
        callbacks.error('Invalid DTMF configuration')
        return
      }
    }
    var dtmf = callbacks.dtmf
    if(!dtmf) {
      Janus.warn('Invalid DTMF parameters')
      callbacks.error('Invalid DTMF parameters')
      return
    }
    var tones = dtmf.tones
    if(!tones) {
      Janus.warn('Invalid DTMF string')
      callbacks.error('Invalid DTMF string')
      return
    }
    var duration = (typeof dtmf.duration === 'number') ? dtmf.duration : 500 // We choose 500ms as the default duration for a tone
    var gap = (typeof dtmf.gap === 'number') ? dtmf.gap : 50 // We choose 50ms as the default gap between tones
    Janus.debug('Sending DTMF string ' + tones + ' (duration ' + duration + 'ms, gap ' + gap + 'ms)')
    config.dtmfSender.insertDTMF(tones, duration, gap)
    callbacks.success()
  }

  // Private method to destroy a plugin handle
  function destroyHandle(handleId, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    var noRequest = (callbacks.noRequest === true)
    Janus.log('Destroying handle ' + handleId + ' (only-locally=' + noRequest + ')')
    cleanupWebrtc(handleId)
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || pluginHandle.detached) {
      // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here
      delete pluginHandles[handleId]
      callbacks.success()
      return
    }
    pluginHandle.detached = true
    if(noRequest) {
      // We're only removing the handle locally
      delete pluginHandles[handleId]
      callbacks.success()
      return
    }
    if(!connected) {
      Janus.warn('Is the server down? (connected=false)')
      callbacks.error('Is the server down? (connected=false)')
      return
    }
    var request = { 'janus': 'detach', 'transaction': Janus.randomString(12) }
    if(pluginHandle.token)
      request['token'] = pluginHandle.token
    if(apisecret)
      request['apisecret'] = apisecret
    if(websockets) {
      request['session_id'] = sessionId
      request['handle_id'] = handleId
      ws.send(JSON.stringify(request))
      delete pluginHandles[handleId]
      callbacks.success()
      return
    }
    Janus.httpAPICall(server + '/' + sessionId + '/' + handleId, {
      verb: 'POST',
      withCredentials: withCredentials,
      body: request,
      success: function(json) {
        Janus.log('Destroyed handle:')
        Janus.debug(json)
        if(json['janus'] !== 'success') {
          Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason) // FIXME
        }
        delete pluginHandles[handleId]
        callbacks.success()
      },
      error: function(textStatus, errorThrown) {
        Janus.error(textStatus + ':', errorThrown) // FIXME
        // We cleanup anyway
        delete pluginHandles[handleId]
        callbacks.success()
      }
    })
  }

  // WebRTC stuff
  function streamsDone(handleId, jsep, media, callbacks, stream) {
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      // Close all tracks if the given stream has been created internally
      if(!callbacks.stream) {
        Janus.stopAllTracks(stream)
      }
      callbacks.error('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    Janus.debug('streamsDone:', stream)
    if(stream) {
      Janus.debug('  -- Audio tracks:', stream.getAudioTracks())
      Janus.debug('  -- Video tracks:', stream.getVideoTracks())
    }
    // We're now capturing the new stream: check if we're updating or if it's a new thing
    var addTracks = false
    if(!config.myStream || !media.update || (config.streamExternal && !media.replaceAudio && !media.replaceVideo)) {
      config.myStream = stream
      addTracks = true
    } else {
      // We only need to update the existing stream
      if(((!media.update && isAudioSendEnabled(media)) || (media.update && (media.addAudio || media.replaceAudio))) &&
          stream.getAudioTracks() && stream.getAudioTracks().length) {
        config.myStream.addTrack(stream.getAudioTracks()[0])
        if(Janus.unifiedPlan) {
          // Use Transceivers
          Janus.log((media.replaceAudio ? 'Replacing' : 'Adding') + ' audio track:', stream.getAudioTracks()[0])
          var audioTransceiver = null
          var transceivers = config.pc.getTransceivers()
          if(transceivers && transceivers.length > 0) {
            for(var t of transceivers) {
              if((t.sender && t.sender.track && t.sender.track.kind === 'audio') ||
                  (t.receiver && t.receiver.track && t.receiver.track.kind === 'audio')) {
                audioTransceiver = t
                break
              }
            }
          }
          if(audioTransceiver && audioTransceiver.sender) {
            audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0])
          } else {
            config.pc.addTrack(stream.getAudioTracks()[0], stream)
          }
        } else {
          Janus.log((media.replaceAudio ? 'Replacing' : 'Adding') + ' audio track:', stream.getAudioTracks()[0])
          config.pc.addTrack(stream.getAudioTracks()[0], stream)
        }
      }
      if(((!media.update && isVideoSendEnabled(media)) || (media.update && (media.addVideo || media.replaceVideo))) &&
          stream.getVideoTracks() && stream.getVideoTracks().length) {
        config.myStream.addTrack(stream.getVideoTracks()[0])
        if(Janus.unifiedPlan) {
          // Use Transceivers
          Janus.log((media.replaceVideo ? 'Replacing' : 'Adding') + ' video track:', stream.getVideoTracks()[0])
          var videoTransceiver = null
          var transceivers = config.pc.getTransceivers()
          if(transceivers && transceivers.length > 0) {
            for(var t of transceivers) {
              if((t.sender && t.sender.track && t.sender.track.kind === 'video') ||
                  (t.receiver && t.receiver.track && t.receiver.track.kind === 'video')) {
                videoTransceiver = t
                break
              }
            }
          }
          if(videoTransceiver && videoTransceiver.sender) {
            videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0])
          } else {
            config.pc.addTrack(stream.getVideoTracks()[0], stream)
          }
        } else {
          Janus.log((media.replaceVideo ? 'Replacing' : 'Adding') + ' video track:', stream.getVideoTracks()[0])
          config.pc.addTrack(stream.getVideoTracks()[0], stream)
        }
      }
    }
    // If we still need to create a PeerConnection, let's do that
    if(!config.pc) {
      var pc_config = {'iceServers': iceServers, 'iceTransportPolicy': iceTransportPolicy, 'bundlePolicy': bundlePolicy}
      if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
        // For Chrome versions before 72, we force a plan-b semantic, and unified-plan otherwise
        pc_config['sdpSemantics'] = (Janus.webRTCAdapter.browserDetails.version < 72) ? 'plan-b' : 'unified-plan'
      }
      var pc_constraints = {
        'optional': [{'DtlsSrtpKeyAgreement': true}]
      }
      if(ipv6Support) {
        pc_constraints.optional.push({'googIPv6':true})
      }
      // Any custom constraint to add?
      if(callbacks.rtcConstraints && typeof callbacks.rtcConstraints === 'object') {
        Janus.debug('Adding custom PeerConnection constraints:', callbacks.rtcConstraints)
        for(var i in callbacks.rtcConstraints) {
          pc_constraints.optional.push(callbacks.rtcConstraints[i])
        }
      }
      if(Janus.webRTCAdapter.browserDetails.browser === 'edge') {
        // This is Edge, enable BUNDLE explicitly
        pc_config.bundlePolicy = 'max-bundle'
      }
      // Check if a sender or receiver transform has been provided
      if(RTCRtpSender && (RTCRtpSender.prototype.createEncodedStreams ||
          (RTCRtpSender.prototype.createEncodedAudioStreams &&
          RTCRtpSender.prototype.createEncodedVideoStreams)) &&
          (callbacks.senderTransforms || callbacks.receiverTransforms)) {
        config.senderTransforms = callbacks.senderTransforms
        config.receiverTransforms = callbacks.receiverTransforms
        pc_config['forceEncodedAudioInsertableStreams'] = true
        pc_config['forceEncodedVideoInsertableStreams'] = true
        pc_config['encodedInsertableStreams'] = true
      }
      Janus.log('Creating PeerConnection')
      Janus.debug(pc_constraints)
      config.pc = new RTCPeerConnection(pc_config, pc_constraints)
      Janus.debug(config.pc)
      if(config.pc.getStats) { // FIXME
        config.volume = {}
      }
      Janus.log('Preparing local SDP and gathering candidates (trickle=' + config.trickle + ')')
      config.pc.oniceconnectionstatechange = function(e) {
        if(config.pc)
          pluginHandle.iceState(config.pc.iceConnectionState)
      }
      config.pc.onicecandidate = function(event) {
        if (!event.candidate ||
            (Janus.webRTCAdapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
          Janus.log('End of candidates.')
          config.iceDone = true
          if(config.trickle === true) {
            // Notify end of candidates
            sendTrickleCandidate(handleId, {'completed': true})
          } else {
            // No trickle, time to send the complete SDP (including all candidates)
            sendSDP(handleId, callbacks)
          }
        } else {
          // JSON.stringify doesn't work on some WebRTC objects anymore
          // See https://code.google.com/p/chromium/issues/detail?id=467366
          var candidate = {
            'candidate': event.candidate.candidate,
            'sdpMid': event.candidate.sdpMid,
            'sdpMLineIndex': event.candidate.sdpMLineIndex
          }
          if(config.trickle === true) {
            // Send candidate
            sendTrickleCandidate(handleId, candidate)
          }
        }
      }
      config.pc.ontrack = function(event) {
        Janus.log('Handling Remote Track')
        Janus.debug(event)
        if(!event.streams)
          return
        config.remoteStream = event.streams[0]
        pluginHandle.onremotestream(config.remoteStream)
        if(event.track.onended)
          return
        if(config.receiverTransforms) {
          var receiverStreams = null
          if(RTCRtpSender.prototype.createEncodedStreams) {
            receiverStreams = event.receiver.createEncodedStreams()
          } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) {
            if(event.track.kind === 'audio' && config.receiverTransforms['audio']) {
              receiverStreams = event.receiver.createEncodedAudioStreams()
            } else if(event.track.kind === 'video' && config.receiverTransforms['video']) {
              receiverStreams = event.receiver.createEncodedVideoStreams()
            }
          }
          if(receiverStreams) {
            console.log(receiverStreams)
            if(receiverStreams.readableStream && receiverStreams.writableStream) {
              receiverStreams.readableStream
                .pipeThrough(config.receiverTransforms[event.track.kind])
                .pipeTo(receiverStreams.writableStream)
            } else if(receiverStreams.readable && receiverStreams.writable) {
              receiverStreams.readable
                .pipeThrough(config.receiverTransforms[event.track.kind])
                .pipeTo(receiverStreams.writable)
            }
          }
        }
        var trackMutedTimeoutId = null
        Janus.log('Adding onended callback to track:', event.track)
        event.track.onended = function(ev) {
          Janus.log('Remote track removed:', ev)
          if(config.remoteStream) {
            clearTimeout(trackMutedTimeoutId)
            config.remoteStream.removeTrack(ev.target)
            pluginHandle.onremotestream(config.remoteStream)
          }
        }
        event.track.onmute = function(ev) {
          Janus.log('Remote track muted:', ev)
          if(config.remoteStream && trackMutedTimeoutId == null) {
            trackMutedTimeoutId = setTimeout(function() {
              Janus.log('Removing remote track')
              if (config.remoteStream) {
                config.remoteStream.removeTrack(ev.target)
                pluginHandle.onremotestream(config.remoteStream)
                pluginHandle.ontrackmuted()
              }
              trackMutedTimeoutId = null
            // Chrome seems to raise mute events only at multiples of 834ms;
            // we set the timeout to three times this value (rounded to 840ms)
            }, 3 * 840)
          }
        }
        event.track.onunmute = function(ev) {
          Janus.log('Remote track flowing again:', ev)
          if(trackMutedTimeoutId != null) {
            clearTimeout(trackMutedTimeoutId)
            trackMutedTimeoutId = null
          } else {
            try {
              config.remoteStream.addTrack(ev.target)
              pluginHandle.onremotestream(config.remoteStream)
              pluginHandle.ontrackunmuted()
            } catch(e) {
              Janus.error(e)
            }
          }
        }
      }
    }
    if(addTracks && stream) {
      Janus.log('Adding local stream')
      var simulcast2 = (callbacks.simulcast2 === true)
      stream.getTracks().forEach(function(track) {
        Janus.log('Adding local track:', track)
        var sender = null
        if(!simulcast2 || track.kind === 'audio') {
          sender = config.pc.addTrack(track, stream)
        } else {
          Janus.log('Enabling rid-based simulcasting:', track)
          var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates)
          var tr = config.pc.addTransceiver(track, {
            direction: 'sendrecv',
            streams: [stream],
            sendEncodings: callbacks.sendEncodings || [
              { rid: 'h', active: true, maxBitrate: maxBitrates.high },
              { rid: 'm', active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 },
              { rid: 'l', active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 }
            ]
          })
          if(tr)
            sender = tr.sender
        }
        // Check if insertable streams are involved
        if(sender && config.senderTransforms) {
          var senderStreams = null
          if(RTCRtpSender.prototype.createEncodedStreams) {
            senderStreams = sender.createEncodedStreams()
          } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) {
            if(sender.track.kind === 'audio' && config.senderTransforms['audio']) {
              senderStreams = sender.createEncodedAudioStreams()
            } else if(sender.track.kind === 'video' && config.senderTransforms['video']) {
              senderStreams = sender.createEncodedVideoStreams()
            }
          }
          if(senderStreams) {
            console.log(senderStreams)
            if(senderStreams.readableStream && senderStreams.writableStream) {
              senderStreams.readableStream
                .pipeThrough(config.senderTransforms[sender.track.kind])
                .pipeTo(senderStreams.writableStream)
            } else if(senderStreams.readable && senderStreams.writable) {
              senderStreams.readable
                .pipeThrough(config.senderTransforms[sender.track.kind])
                .pipeTo(senderStreams.writable)
            }
          }
        }
      })
    }
    // Any data channel to create?
    if(isDataEnabled(media) && !config.dataChannel[Janus.dataChanDefaultLabel]) {
      Janus.log('Creating default data channel')
      createDataChannel(handleId, Janus.dataChanDefaultLabel, null, false)
      config.pc.ondatachannel = function(event) {
        Janus.log('Data channel created by Janus:', event)
        createDataChannel(handleId, event.channel.label, event.channel.protocol, event.channel)
      }
    }
    // If there's a new local stream, let's notify the application
    if(config.myStream) {
      pluginHandle.onlocalstream(config.myStream)
    }
    // Create offer/answer now
    if(!jsep) {
      createOffer(handleId, media, callbacks)
    } else {
      // some firefox vs 420029 fix here, copied from our prev custom release
      var oldsdp = jsep['sdp']
      var pattern = /420029/gi
      var newsdp = oldsdp.replace(pattern,'42e01f')
      Janus.log(newsdp)
      jsep['sdp'] = newsdp
      config.pc.setRemoteDescription(jsep)
        .then(function() {
          Janus.log('Remote description accepted!')
          config.remoteSdp = jsep.sdp
          // Any trickle candidate we cached?
          if(config.candidates && config.candidates.length > 0) {
            for(var i = 0; i< config.candidates.length; i++) {
              var candidate = config.candidates[i]
              Janus.debug('Adding remote candidate:', candidate)
              if(!candidate || candidate.completed === true) {
                // end-of-candidates
                config.pc.addIceCandidate(Janus.endOfCandidates)
              } else {
                // New candidate
                config.pc.addIceCandidate(candidate)
              }
            }
            config.candidates = []
          }
          // Create the answer now
          createAnswer(handleId, media, callbacks)
        }, callbacks.error)
    }
  }

  function prepareWebrtc(handleId, offer, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : webrtcError
    var jsep = callbacks.jsep
    if(offer && jsep) {
      Janus.error('Provided a JSEP to a createOffer')
      callbacks.error('Provided a JSEP to a createOffer')
      return
    } else if(!offer && (!jsep || !jsep.type || !jsep.sdp)) {
      Janus.error('A valid JSEP is required for createAnswer')
      callbacks.error('A valid JSEP is required for createAnswer')
      return
    }
    /* Check that callbacks.media is a (not null) Object */
    callbacks.media = (typeof callbacks.media === 'object' && callbacks.media) ? callbacks.media : { audio: true, video: true }
    var media = callbacks.media
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      callbacks.error('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    config.trickle = isTrickleEnabled(callbacks.trickle)
    // Are we updating a session?
    if(!config.pc) {
      // Nope, new PeerConnection
      media.update = false
      media.keepAudio = false
      media.keepVideo = false
    } else {
      Janus.log('Updating existing media session')
      media.update = true
      // Check if there's anything to add/remove/replace, or if we
      // can go directly to preparing the new SDP offer or answer
      if(callbacks.stream) {
        // External stream: is this the same as the one we were using before?
        if(callbacks.stream !== config.myStream) {
          Janus.log('Renegotiation involves a new external stream')
        }
      } else {
        // Check if there are changes on audio
        if(media.addAudio) {
          media.keepAudio = false
          media.replaceAudio = false
          media.removeAudio = false
          media.audioSend = true
          if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
            Janus.error('Can\'t add audio stream, there already is one')
            callbacks.error('Can\'t add audio stream, there already is one')
            return
          }
        } else if(media.removeAudio) {
          media.keepAudio = false
          media.replaceAudio = false
          media.addAudio = false
          media.audioSend = false
        } else if(media.replaceAudio) {
          media.keepAudio = false
          media.addAudio = false
          media.removeAudio = false
          media.audioSend = true
        }
        if(!config.myStream) {
          // No media stream: if we were asked to replace, it's actually an "add"
          if(media.replaceAudio) {
            media.keepAudio = false
            media.replaceAudio = false
            media.addAudio = true
            media.audioSend = true
          }
          if(isAudioSendEnabled(media)) {
            media.keepAudio = false
            media.addAudio = true
          }
        } else {
          if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
            // No audio track: if we were asked to replace, it's actually an "add"
            if(media.replaceAudio) {
              media.keepAudio = false
              media.replaceAudio = false
              media.addAudio = true
              media.audioSend = true
            }
            if(isAudioSendEnabled(media)) {
              media.keepAudio = false
              media.addAudio = true
            }
          } else {
            // We have an audio track: should we keep it as it is?
            if(isAudioSendEnabled(media) &&
                !media.removeAudio && !media.replaceAudio) {
              media.keepAudio = true
            }
          }
        }
        // Check if there are changes on video
        if(media.addVideo) {
          media.keepVideo = false
          media.replaceVideo = false
          media.removeVideo = false
          media.videoSend = true
          if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
            Janus.error('Can\'t add video stream, there already is one')
            callbacks.error('Can\'t add video stream, there already is one')
            return
          }
        } else if(media.removeVideo) {
          media.keepVideo = false
          media.replaceVideo = false
          media.addVideo = false
          media.videoSend = false
        } else if(media.replaceVideo) {
          media.keepVideo = false
          media.addVideo = false
          media.removeVideo = false
          media.videoSend = true
        }
        if(!config.myStream) {
          // No media stream: if we were asked to replace, it's actually an "add"
          if(media.replaceVideo) {
            media.keepVideo = false
            media.replaceVideo = false
            media.addVideo = true
            media.videoSend = true
          }
          if(isVideoSendEnabled(media)) {
            media.keepVideo = false
            media.addVideo = true
          }
        } else {
          if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
            // No video track: if we were asked to replace, it's actually an "add"
            if(media.replaceVideo) {
              media.keepVideo = false
              media.replaceVideo = false
              media.addVideo = true
              media.videoSend = true
            }
            if(isVideoSendEnabled(media)) {
              media.keepVideo = false
              media.addVideo = true
            }
          } else {
            // We have a video track: should we keep it as it is?
            if(isVideoSendEnabled(media) && !media.removeVideo && !media.replaceVideo) {
              media.keepVideo = true
            }
          }
        }
        // Data channels can only be added
        if(media.addData) {
          media.data = true
        }
      }
      // If we're updating and keeping all tracks, let's skip the getUserMedia part
      if((isAudioSendEnabled(media) && media.keepAudio) &&
          (isVideoSendEnabled(media) && media.keepVideo)) {
        pluginHandle.consentDialog(false)
        streamsDone(handleId, jsep, media, callbacks, config.myStream)
        return
      }
    }
    // If we're updating, check if we need to remove/replace one of the tracks
    if(media.update && (!config.streamExternal || (config.streamExternal && (media.replaceAudio || media.replaceVideo)))) {
      if(media.removeAudio || media.replaceAudio) {
        if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
          var at = config.myStream.getAudioTracks()[0]
          Janus.log('Removing audio track:', at)
          config.myStream.removeTrack(at)
          try {
            at.stop()
          } catch(e) {}
        }
        if(config.pc.getSenders() && config.pc.getSenders().length) {
          var ra = true
          if(media.replaceAudio && Janus.unifiedPlan) {
            // We can use replaceTrack
            ra = false
          }
          if(ra) {
            for(var asnd of config.pc.getSenders()) {
              if(asnd && asnd.track && asnd.track.kind === 'audio') {
                Janus.log('Removing audio sender:', asnd)
                config.pc.removeTrack(asnd)
              }
            }
          }
        }
      }
      if(media.removeVideo || media.replaceVideo) {
        if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
          var vt = config.myStream.getVideoTracks()[0]
          Janus.log('Removing video track:', vt)
          config.myStream.removeTrack(vt)
          try {
            vt.stop()
          } catch(e) {}
        }
        if(config.pc.getSenders() && config.pc.getSenders().length) {
          var rv = true
          if(media.replaceVideo && Janus.unifiedPlan) {
            // We can use replaceTrack
            rv = false
          }
          if(rv) {
            for(var vsnd of config.pc.getSenders()) {
              if(vsnd && vsnd.track && vsnd.track.kind === 'video') {
                Janus.log('Removing video sender:', vsnd)
                config.pc.removeTrack(vsnd)
              }
            }
          }
        }
      }
    }
    // Was a MediaStream object passed, or do we need to take care of that?
    if(callbacks.stream) {
      var stream = callbacks.stream
      Janus.log('MediaStream provided by the application')
      Janus.debug(stream)
      // If this is an update, let's check if we need to release the previous stream
      if(media.update && config.myStream && config.myStream !== callbacks.stream && !config.streamExternal && !media.replaceAudio && !media.replaceVideo) {
        // We're replacing a stream we captured ourselves with an external one
        Janus.stopAllTracks(config.myStream)
        config.myStream = null
      }
      // Skip the getUserMedia part
      config.streamExternal = true
      pluginHandle.consentDialog(false)
      streamsDone(handleId, jsep, media, callbacks, stream)
      return
    }
    if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) {
      if(!Janus.isGetUserMediaAvailable()) {
        callbacks.error('getUserMedia not available')
        return
      }
      var constraints = { mandatory: {}, optional: []}
      pluginHandle.consentDialog(true)
      var audioSupport = isAudioSendEnabled(media)
      if(audioSupport && media && typeof media.audio === 'object')
        audioSupport = media.audio
      var videoSupport = isVideoSendEnabled(media)
      if(videoSupport && media) {
        var simulcast = (callbacks.simulcast === true)
        var simulcast2 = (callbacks.simulcast2 === true)
        if((simulcast || simulcast2) && !jsep && !media.video)
          media.video = 'hires'
        if(media.video && media.video != 'screen' && media.video != 'window') {
          if(typeof media.video === 'object') {
            videoSupport = media.video
          } else {
            var width = 0
            var height = 0, maxHeight = 0
            if(media.video === 'lowres') {
              // Small resolution, 4:3
              height = 240
              maxHeight = 240
              width = 320
            } else if(media.video === 'lowres-16:9') {
              // Small resolution, 16:9
              height = 180
              maxHeight = 180
              width = 320
            } else if(media.video === 'hires' || media.video === 'hires-16:9' || media.video === 'hdres') {
              // High(HD) resolution is only 16:9
              height = 720
              maxHeight = 720
              width = 1280
            } else if(media.video === 'fhdres') {
              // Full HD resolution is only 16:9
              height = 1080
              maxHeight = 1080
              width = 1920
            } else if(media.video === '4kres') {
              // 4K resolution is only 16:9
              height = 2160
              maxHeight = 2160
              width = 3840
            } else if(media.video === 'stdres') {
              // Normal resolution, 4:3
              height = 480
              maxHeight = 480
              width = 640
            } else if(media.video === 'stdres-16:9') {
              // Normal resolution, 16:9
              height = 360
              maxHeight = 360
              width = 640
            } else {
              Janus.log('Default video setting is stdres 4:3')
              height = 480
              maxHeight = 480
              width = 640
            }
            Janus.log('Adding media constraint:', media.video)
            videoSupport = {
              'height': {'ideal': height},
              'width': {'ideal': width}
            }
            Janus.log('Adding video constraint:', videoSupport)
          }
        } else if(media.video === 'screen' || media.video === 'window') {
          if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
            // The new experimental getDisplayMedia API is available, let's use that
            // https://groups.google.com/forum/#!topic/discuss-webrtc/Uf0SrR4uxzk
            // https://webrtchacks.com/chrome-screensharing-getdisplaymedia/
            constraints.video = {}
            if(media.screenshareFrameRate) {
              constraints.video.frameRate = media.screenshareFrameRate
            }
            if(media.screenshareHeight) {
              constraints.video.height = media.screenshareHeight
            }
            if(media.screenshareWidth) {
              constraints.video.width = media.screenshareWidth
            }
            constraints.audio = media.captureDesktopAudio
            navigator.mediaDevices.getDisplayMedia(constraints)
              .then(function(stream) {
                pluginHandle.consentDialog(false)
                if(isAudioSendEnabled(media) && !media.keepAudio) {
                  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
                    .then(function (audioStream) {
                      stream.addTrack(audioStream.getAudioTracks()[0])
                      streamsDone(handleId, jsep, media, callbacks, stream)
                    })
                } else {
                  streamsDone(handleId, jsep, media, callbacks, stream)
                }
              }, function (error) {
                pluginHandle.consentDialog(false)
                callbacks.error(error)
              })
            return
          }
          // We're going to try and use the extension for Chrome 34+, the old approach
          // for older versions of Chrome, or the experimental support in Firefox 33+
          function callbackUserMedia (error, stream) {
            pluginHandle.consentDialog(false)
            if(error) {
              callbacks.error(error)
            } else {
              streamsDone(handleId, jsep, media, callbacks, stream)
            }
          }
          function getScreenMedia(constraints, gsmCallback, useAudio) {
            Janus.log('Adding media constraint (screen capture)')
            Janus.debug(constraints)
            navigator.mediaDevices.getUserMedia(constraints)
              .then(function(stream) {
                if(useAudio) {
                  navigator.mediaDevices.getUserMedia({ audio: true, video: false })
                    .then(function (audioStream) {
                      stream.addTrack(audioStream.getAudioTracks()[0])
                      gsmCallback(null, stream)
                    })
                } else {
                  gsmCallback(null, stream)
                }
              })
              .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error) })
          }
          if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
            var chromever = Janus.webRTCAdapter.browserDetails.version
            var maxver = 33
            if(window.navigator.userAgent.match('Linux'))
              maxver = 35 // "known" crash in chrome 34 and 35 on linux
            if(chromever >= 26 && chromever <= maxver) {
              // Chrome 26->33 requires some awkward chrome://flags manipulation
              constraints = {
                video: {
                  mandatory: {
                    googLeakyBucket: true,
                    maxWidth: window.screen.width,
                    maxHeight: window.screen.height,
                    minFrameRate: media.screenshareFrameRate,
                    maxFrameRate: media.screenshareFrameRate,
                    chromeMediaSource: 'screen'
                  }
                },
                audio: isAudioSendEnabled(media) && !media.keepAudio
              }
              getScreenMedia(constraints, callbackUserMedia)
            } else {
              // Chrome 34+ requires an extension
              Janus.extension.getScreen(function (error, sourceId) {
                if (error) {
                  pluginHandle.consentDialog(false)
                  return callbacks.error(error)
                }
                constraints = {
                  audio: false,
                  video: {
                    mandatory: {
                      chromeMediaSource: 'desktop',
                      maxWidth: window.screen.width,
                      maxHeight: window.screen.height,
                      minFrameRate: media.screenshareFrameRate,
                      maxFrameRate: media.screenshareFrameRate,
                    },
                    optional: [
                      {googLeakyBucket: true},
                      {googTemporalLayeredScreencast: true}
                    ]
                  }
                }
                constraints.video.mandatory.chromeMediaSourceId = sourceId
                getScreenMedia(constraints, callbackUserMedia,
                  isAudioSendEnabled(media) && !media.keepAudio)
              })
            }
          } else if(Janus.webRTCAdapter.browserDetails.browser === 'firefox') {
            if(Janus.webRTCAdapter.browserDetails.version >= 33) {
              // Firefox 33+ has experimental support for screen sharing
              constraints = {
                video: {
                  mozMediaSource: media.video,
                  mediaSource: media.video
                },
                audio: isAudioSendEnabled(media) && !media.keepAudio
              }
              getScreenMedia(constraints, function (err, stream) {
                callbackUserMedia(err, stream)
                // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810
                if (!err) {
                  var lastTime = stream.currentTime
                  var polly = window.setInterval(function () {
                    if(!stream)
                      window.clearInterval(polly)
                    if(stream.currentTime == lastTime) {
                      window.clearInterval(polly)
                      if(stream.onended) {
                        stream.onended()
                      }
                    }
                    lastTime = stream.currentTime
                  }, 500)
                }
              })
            } else {
              var error = new Error('NavigatorUserMediaError')
              error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)'
              pluginHandle.consentDialog(false)
              callbacks.error(error)
              return
            }
          }
          return
        }
      }
      // If we got here, we're not screensharing
      if(!media || media.video !== 'screen') {
        // Check whether all media sources are actually available or not
        navigator.mediaDevices.enumerateDevices().then(function(devices) {
          var audioExist = devices.some(function(device) {
              return device.kind === 'audioinput'
            }),
            videoExist = isScreenSendEnabled(media) || devices.some(function(device) {
              return device.kind === 'videoinput'
            })

          // Check whether a missing device is really a problem
          var audioSend = isAudioSendEnabled(media)
          var videoSend = isVideoSendEnabled(media)
          var needAudioDevice = isAudioSendRequired(media)
          var needVideoDevice = isVideoSendRequired(media)
          if(audioSend || videoSend || needAudioDevice || needVideoDevice) {
            // We need to send either audio or video
            var haveAudioDevice = audioSend ? audioExist : false
            var haveVideoDevice = videoSend ? videoExist : false
            if(!haveAudioDevice && !haveVideoDevice) {
              // FIXME Should we really give up, or just assume recvonly for both?
              pluginHandle.consentDialog(false)
              callbacks.error('No capture device found')
              return false
            } else if(!haveAudioDevice && needAudioDevice) {
              pluginHandle.consentDialog(false)
              callbacks.error('Audio capture is required, but no capture device found')
              return false
            } else if(!haveVideoDevice && needVideoDevice) {
              pluginHandle.consentDialog(false)
              callbacks.error('Video capture is required, but no capture device found')
              return false
            }
          }

          var gumConstraints = {
            audio: (audioExist && !media.keepAudio) ? audioSupport : false,
            video: (videoExist && !media.keepVideo) ? videoSupport : false
          }
          Janus.debug('getUserMedia constraints', gumConstraints)
          if (!gumConstraints.audio && !gumConstraints.video) {
            pluginHandle.consentDialog(false)
            streamsDone(handleId, jsep, media, callbacks, stream)
          } else {
            navigator.mediaDevices.getUserMedia(gumConstraints)
              .then(function(stream) {
                pluginHandle.consentDialog(false)
                streamsDone(handleId, jsep, media, callbacks, stream)
              }).catch(function(error) {
                pluginHandle.consentDialog(false)
                callbacks.error({code: error.code, name: error.name, message: error.message})
              })
          }
        })
          .catch(function(error) {
            pluginHandle.consentDialog(false)
            callbacks.error(error)
          })
      }
    } else {
      // No need to do a getUserMedia, create offer/answer right away
      streamsDone(handleId, jsep, media, callbacks)
    }
  }

  function prepareWebrtcPeer(handleId, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : webrtcError
    callbacks.customizeSdp = (typeof callbacks.customizeSdp == 'function') ? callbacks.customizeSdp : Janus.noop
    var jsep = callbacks.jsep
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      callbacks.error('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    if(jsep) {
      if(!config.pc) {
        Janus.warn('Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep')
        callbacks.error('No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep')
        return
      }
      callbacks.customizeSdp(jsep)
      config.pc.setRemoteDescription(jsep)
        .then(function() {
          Janus.log('Remote description accepted!')
          config.remoteSdp = jsep.sdp
          // Any trickle candidate we cached?
          if(config.candidates && config.candidates.length > 0) {
            for(var i = 0; i< config.candidates.length; i++) {
              var candidate = config.candidates[i]
              Janus.debug('Adding remote candidate:', candidate)
              if(!candidate || candidate.completed === true) {
                // end-of-candidates
                config.pc.addIceCandidate(Janus.endOfCandidates)
              } else {
                // New candidate
                config.pc.addIceCandidate(candidate)
              }
            }
            config.candidates = []
          }
          // Done
          callbacks.success()
        }, callbacks.error)
    } else {
      callbacks.error('Invalid JSEP')
    }
  }

  function createOffer(handleId, media, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    callbacks.customizeSdp = (typeof callbacks.customizeSdp == 'function') ? callbacks.customizeSdp : Janus.noop
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      callbacks.error('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    var simulcast = (callbacks.simulcast === true)
    if(!simulcast) {
      Janus.log('Creating offer (iceDone=' + config.iceDone + ')')
    } else {
      Janus.log('Creating offer (iceDone=' + config.iceDone + ', simulcast=' + simulcast + ')')
    }
    // https://code.google.com/p/webrtc/issues/detail?id=3508
    var mediaConstraints = {}
    if(Janus.unifiedPlan) {
      // We can use Transceivers
      var audioTransceiver = null, videoTransceiver = null
      var transceivers = config.pc.getTransceivers()
      if(transceivers && transceivers.length > 0) {
        for(var t of transceivers) {
          if((t.sender && t.sender.track && t.sender.track.kind === 'audio') ||
              (t.receiver && t.receiver.track && t.receiver.track.kind === 'audio')) {
            if(!audioTransceiver) {
              audioTransceiver = t
            }
            continue
          }
          if((t.sender && t.sender.track && t.sender.track.kind === 'video') ||
              (t.receiver && t.receiver.track && t.receiver.track.kind === 'video')) {
            if(!videoTransceiver) {
              videoTransceiver = t
            }
            continue
          }
        }
      }
      // Handle audio (and related changes, if any)
      var audioSend = isAudioSendEnabled(media)
      var audioRecv = isAudioRecvEnabled(media)
      if(!audioSend && !audioRecv) {
        // Audio disabled: have we removed it?
        if(media.removeAudio && audioTransceiver) {
          if (audioTransceiver.setDirection) {
            audioTransceiver.setDirection('inactive')
          } else {
            audioTransceiver.direction = 'inactive'
          }
          Janus.log('Setting audio transceiver to inactive:', audioTransceiver)
        }
      } else {
        // Take care of audio m-line
        if(audioSend && audioRecv) {
          if(audioTransceiver) {
            if (audioTransceiver.setDirection) {
              audioTransceiver.setDirection('sendrecv')
            } else {
              audioTransceiver.direction = 'sendrecv'
            }
            Janus.log('Setting audio transceiver to sendrecv:', audioTransceiver)
          }
        } else if(audioSend && !audioRecv) {
          if(audioTransceiver) {
            if (audioTransceiver.setDirection) {
              audioTransceiver.setDirection('sendonly')
            } else {
              audioTransceiver.direction = 'sendonly'
            }
            Janus.log('Setting audio transceiver to sendonly:', audioTransceiver)
          }
        } else if(!audioSend && audioRecv) {
          if(audioTransceiver) {
            if (audioTransceiver.setDirection) {
              audioTransceiver.setDirection('recvonly')
            } else {
              audioTransceiver.direction = 'recvonly'
            }
            Janus.log('Setting audio transceiver to recvonly:', audioTransceiver)
          } else {
            // In theory, this is the only case where we might not have a transceiver yet
            audioTransceiver = config.pc.addTransceiver('audio', { direction: 'recvonly' })
            Janus.log('Adding recvonly audio transceiver:', audioTransceiver)
          }
        }
      }
      // Handle video (and related changes, if any)
      var videoSend = isVideoSendEnabled(media)
      var videoRecv = isVideoRecvEnabled(media)
      if(!videoSend && !videoRecv) {
        // Video disabled: have we removed it?
        if(media.removeVideo && videoTransceiver) {
          if (videoTransceiver.setDirection) {
            videoTransceiver.setDirection('inactive')
          } else {
            videoTransceiver.direction = 'inactive'
          }
          Janus.log('Setting video transceiver to inactive:', videoTransceiver)
        }
      } else {
        // Take care of video m-line
        if(videoSend && videoRecv) {
          if(videoTransceiver) {
            if (videoTransceiver.setDirection) {
              videoTransceiver.setDirection('sendrecv')
            } else {
              videoTransceiver.direction = 'sendrecv'
            }
            Janus.log('Setting video transceiver to sendrecv:', videoTransceiver)
          }
        } else if(videoSend && !videoRecv) {
          if(videoTransceiver) {
            if (videoTransceiver.setDirection) {
              videoTransceiver.setDirection('sendonly')
            } else {
              videoTransceiver.direction = 'sendonly'
            }
            Janus.log('Setting video transceiver to sendonly:', videoTransceiver)
          }
        } else if(!videoSend && videoRecv) {
          if(videoTransceiver) {
            if (videoTransceiver.setDirection) {
              videoTransceiver.setDirection('recvonly')
            } else {
              videoTransceiver.direction = 'recvonly'
            }
            Janus.log('Setting video transceiver to recvonly:', videoTransceiver)
          } else {
            // In theory, this is the only case where we might not have a transceiver yet
            videoTransceiver = config.pc.addTransceiver('video', { direction: 'recvonly' })
            Janus.log('Adding recvonly video transceiver:', videoTransceiver)
          }
        }
      }
    } else {
      mediaConstraints['offerToReceiveAudio'] = isAudioRecvEnabled(media)
      mediaConstraints['offerToReceiveVideo'] = isVideoRecvEnabled(media)
    }
    var iceRestart = (callbacks.iceRestart === true)
    if(iceRestart) {
      mediaConstraints['iceRestart'] = true
    }
    Janus.debug(mediaConstraints)
    // Check if this is Firefox and we've been asked to do simulcasting
    var sendVideo = isVideoSendEnabled(media)
    if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === 'firefox') {
      // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
      Janus.log('Enabling Simulcasting for Firefox (RID)')
      var sender = config.pc.getSenders().find(function(s) {return s.track && s.track.kind === 'video'})
      if(sender) {
        var parameters = sender.getParameters()
        if(!parameters) {
          parameters = {}
        }
        var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates)
        parameters.encodings = callbacks.sendEncodings || [
          { rid: 'h', active: true, maxBitrate: maxBitrates.high },
          { rid: 'm', active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 },
          { rid: 'l', active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 }
        ]
        sender.setParameters(parameters)
      }
    }
    config.pc.createOffer(mediaConstraints)
      .then(function(offer) {
        Janus.debug(offer)
        // JSON.stringify doesn't work on some WebRTC objects anymore
        // See https://code.google.com/p/chromium/issues/detail?id=467366
        var jsep = {
          'type': offer.type,
          'sdp': offer.sdp
        }
        callbacks.customizeSdp(jsep)
        offer.sdp = jsep.sdp
        Janus.log('Setting local description')
        if(sendVideo && simulcast) {
          // This SDP munging only works with Chrome (Safari STP may support it too)
          if(Janus.webRTCAdapter.browserDetails.browser === 'chrome' ||
              Janus.webRTCAdapter.browserDetails.browser === 'safari') {
            Janus.log('Enabling Simulcasting for Chrome (SDP munging)')
            offer.sdp = mungeSdpForSimulcasting(offer.sdp)
          } else if(Janus.webRTCAdapter.browserDetails.browser !== 'firefox') {
            Janus.warn('simulcast=true, but this is not Chrome nor Firefox, ignoring')
          }
        }
        config.mySdp = {
          type: 'offer',
          sdp: offer.sdp
        }
        config.pc.setLocalDescription(offer)
          .catch(callbacks.error)
        config.mediaConstraints = mediaConstraints
        if(!config.iceDone && !config.trickle) {
          // Don't do anything until we have all candidates
          Janus.log('Waiting for all candidates...')
          return
        }
        // If transforms are present, notify Janus that the media is end-to-end encrypted
        if(config.senderTransforms || config.receiverTransforms) {
          offer['e2ee'] = true
        }
        callbacks.success(offer)
      }, callbacks.error)
  }

  function createAnswer(handleId, media, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    callbacks.customizeSdp = (typeof callbacks.customizeSdp == 'function') ? callbacks.customizeSdp : Janus.noop
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      callbacks.error('Invalid handle')
      return
    }
    var config = pluginHandle.webrtcStuff
    var simulcast = (callbacks.simulcast === true)
    if(!simulcast) {
      Janus.log('Creating answer (iceDone=' + config.iceDone + ')')
    } else {
      Janus.log('Creating answer (iceDone=' + config.iceDone + ', simulcast=' + simulcast + ')')
    }
    var mediaConstraints = null
    if(Janus.unifiedPlan) {
      // We can use Transceivers
      mediaConstraints = {}
      var audioTransceiver = null, videoTransceiver = null
      var transceivers = config.pc.getTransceivers()
      if(transceivers && transceivers.length > 0) {
        for(var t of transceivers) {
          if((t.sender && t.sender.track && t.sender.track.kind === 'audio') ||
              (t.receiver && t.receiver.track && t.receiver.track.kind === 'audio')) {
            if(!audioTransceiver)
              audioTransceiver = t
            continue
          }
          if((t.sender && t.sender.track && t.sender.track.kind === 'video') ||
              (t.receiver && t.receiver.track && t.receiver.track.kind === 'video')) {
            if(!videoTransceiver)
              videoTransceiver = t
            continue
          }
        }
      }
      // Handle audio (and related changes, if any)
      var audioSend = isAudioSendEnabled(media)
      var audioRecv = isAudioRecvEnabled(media)
      if(!audioSend && !audioRecv) {
        // Audio disabled: have we removed it?
        if(media.removeAudio && audioTransceiver) {
          try {
            if (audioTransceiver.setDirection) {
              audioTransceiver.setDirection('inactive')
            } else {
              audioTransceiver.direction = 'inactive'
            }
            Janus.log('Setting audio transceiver to inactive:', audioTransceiver)
          } catch(e) {
            Janus.error(e)
          }
        }
      } else {
        // Take care of audio m-line
        if(audioSend && audioRecv) {
          if(audioTransceiver) {
            try {
              if (audioTransceiver.setDirection) {
                audioTransceiver.setDirection('sendrecv')
              } else {
                audioTransceiver.direction = 'sendrecv'
              }
              Janus.log('Setting audio transceiver to sendrecv:', audioTransceiver)
            } catch(e) {
              Janus.error(e)
            }
          }
        } else if(audioSend && !audioRecv) {
          try {
            if(audioTransceiver) {
              if (audioTransceiver.setDirection) {
                audioTransceiver.setDirection('sendonly')
              } else {
                audioTransceiver.direction = 'sendonly'
              }
              Janus.log('Setting audio transceiver to sendonly:', audioTransceiver)
            }
          } catch(e) {
            Janus.error(e)
          }
        } else if(!audioSend && audioRecv) {
          if(audioTransceiver) {
            try {
              if (audioTransceiver.setDirection) {
                audioTransceiver.setDirection('recvonly')
              } else {
                audioTransceiver.direction = 'recvonly'
              }
              Janus.log('Setting audio transceiver to recvonly:', audioTransceiver)
            } catch(e) {
              Janus.error(e)
            }
          } else {
            // In theory, this is the only case where we might not have a transceiver yet
            audioTransceiver = config.pc.addTransceiver('audio', { direction: 'recvonly' })
            Janus.log('Adding recvonly audio transceiver:', audioTransceiver)
          }
        }
      }
      // Handle video (and related changes, if any)
      var videoSend = isVideoSendEnabled(media)
      var videoRecv = isVideoRecvEnabled(media)
      if(!videoSend && !videoRecv) {
        // Video disabled: have we removed it?
        if(media.removeVideo && videoTransceiver) {
          try {
            if (videoTransceiver.setDirection) {
              videoTransceiver.setDirection('inactive')
            } else {
              videoTransceiver.direction = 'inactive'
            }
            Janus.log('Setting video transceiver to inactive:', videoTransceiver)
          } catch(e) {
            Janus.error(e)
          }
        }
      } else {
        // Take care of video m-line
        if(videoSend && videoRecv) {
          if(videoTransceiver) {
            try {
              if (videoTransceiver.setDirection) {
                videoTransceiver.setDirection('sendrecv')
              } else {
                videoTransceiver.direction = 'sendrecv'
              }
              Janus.log('Setting video transceiver to sendrecv:', videoTransceiver)
            } catch(e) {
              Janus.error(e)
            }
          }
        } else if(videoSend && !videoRecv) {
          if(videoTransceiver) {
            try {
              if (videoTransceiver.setDirection) {
                videoTransceiver.setDirection('sendonly')
              } else {
                videoTransceiver.direction = 'sendonly'
              }
              Janus.log('Setting video transceiver to sendonly:', videoTransceiver)
            } catch(e) {
              Janus.error(e)
            }
          }
        } else if(!videoSend && videoRecv) {
          if(videoTransceiver) {
            try {
              if (videoTransceiver.setDirection) {
                videoTransceiver.setDirection('recvonly')
              } else {
                videoTransceiver.direction = 'recvonly'
              }
              Janus.log('Setting video transceiver to recvonly:', videoTransceiver)
            } catch(e) {
              Janus.error(e)
            }
          } else {
            // In theory, this is the only case where we might not have a transceiver yet
            videoTransceiver = config.pc.addTransceiver('video', { direction: 'recvonly' })
            Janus.log('Adding recvonly video transceiver:', videoTransceiver)
          }
        }
      }
    } else {
      if(Janus.webRTCAdapter.browserDetails.browser === 'firefox' || Janus.webRTCAdapter.browserDetails.browser === 'edge') {
        mediaConstraints = {
          offerToReceiveAudio: isAudioRecvEnabled(media),
          offerToReceiveVideo: isVideoRecvEnabled(media)
        }
      } else {
        mediaConstraints = {
          mandatory: {
            OfferToReceiveAudio: isAudioRecvEnabled(media),
            OfferToReceiveVideo: isVideoRecvEnabled(media)
          }
        }
      }
    }
    Janus.debug(mediaConstraints)
    // Check if this is Firefox and we've been asked to do simulcasting
    var sendVideo = isVideoSendEnabled(media)
    if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === 'firefox') {
      // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
      Janus.log('Enabling Simulcasting for Firefox (RID)')
      var sender = config.pc.getSenders()[1]
      Janus.log(sender)
      var parameters = sender.getParameters()
      Janus.log(parameters)

      var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates)
      sender.setParameters({encodings: callbacks.sendEncodings || [
        { rid: 'h', active: true, maxBitrate: maxBitrates.high },
        { rid: 'm', active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2},
        { rid: 'l', active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4}
      ]})
    }
    config.pc.createAnswer(mediaConstraints)
      .then(function(answer) {
        Janus.debug(answer)
        // JSON.stringify doesn't work on some WebRTC objects anymore
        // See https://code.google.com/p/chromium/issues/detail?id=467366
        var jsep = {
          'type': answer.type,
          'sdp': answer.sdp
        }
        callbacks.customizeSdp(jsep)
        answer.sdp = jsep.sdp
        Janus.log('Setting local description')
        if(sendVideo && simulcast) {
          // This SDP munging only works with Chrome
          if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
            // FIXME Apparently trying to simulcast when answering breaks video in Chrome...
            //~ Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
            //~ answer.sdp = mungeSdpForSimulcasting(answer.sdp);
            Janus.warn('simulcast=true, but this is an answer, and video breaks in Chrome if we enable it')
          } else if(Janus.webRTCAdapter.browserDetails.browser !== 'firefox') {
            Janus.warn('simulcast=true, but this is not Chrome nor Firefox, ignoring')
          }
        }
        config.mySdp = {
          type: 'answer',
          sdp: answer.sdp
        }
        config.pc.setLocalDescription(answer)
          .catch(callbacks.error)
        config.mediaConstraints = mediaConstraints
        if(!config.iceDone && !config.trickle) {
          // Don't do anything until we have all candidates
          Janus.log('Waiting for all candidates...')
          return
        }
        // If transforms are present, notify Janus that the media is end-to-end encrypted
        if(config.senderTransforms || config.receiverTransforms) {
          answer['e2ee'] = true
        }
        callbacks.success(answer)
      }, callbacks.error)
  }

  function sendSDP(handleId, callbacks) {
    callbacks = callbacks || {}
    callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : Janus.noop
    callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : Janus.noop
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle, not sending anything')
      return
    }
    var config = pluginHandle.webrtcStuff
    Janus.log('Sending offer/answer SDP...')
    if(!config.mySdp) {
      Janus.warn('Local SDP instance is invalid, not sending anything...')
      return
    }
    config.mySdp = {
      'type': config.pc.localDescription.type,
      'sdp': config.pc.localDescription.sdp
    }
    if(config.trickle === false)
      config.mySdp['trickle'] = false
    Janus.debug(callbacks)
    config.sdpSent = true
    callbacks.success(config.mySdp)
  }

  function getVolume(handleId, remote) {
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      return 0
    }
    var stream = remote ? 'remote' : 'local'
    var config = pluginHandle.webrtcStuff
    if(!config.volume[stream])
      config.volume[stream] = { value: 0 }
    // Start getting the volume, if audioLevel in getStats is supported (apparently
    // they're only available in Chrome/Safari right now: https://webrtc-stats.callstats.io/)
    if(config.pc.getStats && (Janus.webRTCAdapter.browserDetails.browser === 'chrome' ||
        Janus.webRTCAdapter.browserDetails.browser === 'safari')) {
      if(remote && !config.remoteStream) {
        Janus.warn('Remote stream unavailable')
        return 0
      } else if(!remote && !config.myStream) {
        Janus.warn('Local stream unavailable')
        return 0
      }
      if(!config.volume[stream].timer) {
        Janus.log('Starting ' + stream + ' volume monitor')
        config.volume[stream].timer = setInterval(function() {
          config.pc.getStats()
            .then(function(stats) {
              stats.forEach(function (res) {
                if(!res || res.kind !== 'audio')
                  return
                if((remote && !res.remoteSource) || (!remote && res.type !== 'media-source'))
                  return
                config.volume[stream].value = (res.audioLevel ? res.audioLevel : 0)
              })
            })
        }, 200)
        return 0 // We don't have a volume to return yet
      }
      return config.volume[stream].value
    } else {
      // audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel
      // seems to be available on Chrome and Firefox, but they don't seem to work
      Janus.warn('Getting the ' + stream + ' volume unsupported by browser')
      return 0
    }
  }

  function isMuted(handleId, video) {
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      return true
    }
    var config = pluginHandle.webrtcStuff
    if(!config.pc) {
      Janus.warn('Invalid PeerConnection')
      return true
    }
    if(!config.myStream) {
      Janus.warn('Invalid local MediaStream')
      return true
    }
    if(video) {
      // Check video track
      if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
        Janus.warn('No video track')
        return true
      }
      return !config.myStream.getVideoTracks()[0].enabled
    } else {
      // Check audio track
      if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
        Janus.warn('No audio track')
        return true
      }
      return !config.myStream.getAudioTracks()[0].enabled
    }
  }

  function mute(handleId, video, mute) {
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      return false
    }
    var config = pluginHandle.webrtcStuff
    if(!config.pc) {
      Janus.warn('Invalid PeerConnection')
      return false
    }
    if(!config.myStream) {
      Janus.warn('Invalid local MediaStream')
      return false
    }
    if(video) {
      // Mute/unmute video track
      if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
        Janus.warn('No video track')
        return false
      }
      config.myStream.getVideoTracks()[0].enabled = !mute
      return true
    } else {
      // Mute/unmute audio track
      if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
        Janus.warn('No audio track')
        return false
      }
      config.myStream.getAudioTracks()[0].enabled = !mute
      return true
    }
  }

  function getBitrate(handleId) {
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle || !pluginHandle.webrtcStuff) {
      Janus.warn('Invalid handle')
      return 'Invalid handle'
    }
    var config = pluginHandle.webrtcStuff
    if(!config.pc)
      return 'Invalid PeerConnection'
    // Start getting the bitrate, if getStats is supported
    if(config.pc.getStats) {
      if(!config.bitrate.timer) {
        Janus.log('Starting bitrate timer (via getStats)')
        config.bitrate.timer = setInterval(function() {
          config.pc.getStats()
            .then(function(stats) {
              stats.forEach(function (res) {
                if(!res)
                  return
                var inStats = false
                // Check if these are statistics on incoming media
                if((res.mediaType === 'video' || res.id.toLowerCase().indexOf('video') > -1) &&
                    res.type === 'inbound-rtp' && res.id.indexOf('rtcp') < 0) {
                  // New stats
                  inStats = true
                } else if(res.type == 'ssrc' && res.bytesReceived &&
                    (res.googCodecName === 'VP8' || res.googCodecName === '')) {
                  // Older Chromer versions
                  inStats = true
                }
                // Parse stats now
                if(inStats) {
                  config.bitrate.fps = res.framesPerSecond
                  config.bitrate.bsnow = res.bytesReceived
                  config.bitrate.tsnow = res.timestamp
                  if(config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) {
                    // Skip this round
                    config.bitrate.bsbefore = config.bitrate.bsnow
                    config.bitrate.tsbefore = config.bitrate.tsnow
                  } else {
                    // Calculate bitrate
                    var timePassed = config.bitrate.tsnow - config.bitrate.tsbefore
                    if(Janus.webRTCAdapter.browserDetails.browser === 'safari')
                      timePassed = timePassed/1000 // Apparently the timestamp is in microseconds, in Safari
                    var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / timePassed)
                    if(Janus.webRTCAdapter.browserDetails.browser === 'safari')
                      bitRate = parseInt(bitRate/1000)
                    config.bitrate.value = bitRate + ' kbps'
                    //~ Janus.log("Estimated bitrate is " + config.bitrate.value);
                    config.bitrate.bsbefore = config.bitrate.bsnow
                    config.bitrate.tsbefore = config.bitrate.tsnow
                  }
                }
              })
            })
        }, 1000)
      }
      return config.bitrate
    } else {
      Janus.warn('Getting the video bitrate unsupported by browser')
      return 'Feature unsupported by browser'
    }
  }

  function webrtcError(error) {
    Janus.error('WebRTC error:', error)
  }

  function cleanupWebrtc(handleId, hangupRequest) {
    Janus.log('Cleaning WebRTC stuff')
    var pluginHandle = pluginHandles[handleId]
    if(!pluginHandle) {
      // Nothing to clean
      return
    }
    var config = pluginHandle.webrtcStuff
    if(config) {
      if(hangupRequest === true) {
        // Send a hangup request (we don't really care about the response)
        var request = { 'janus': 'hangup', 'transaction': Janus.randomString(12) }
        if(pluginHandle.token)
          request['token'] = pluginHandle.token
        if(apisecret)
          request['apisecret'] = apisecret
        Janus.debug('Sending hangup request (handle=' + handleId + '):')
        Janus.debug(request)
        if(websockets) {
          request['session_id'] = sessionId
          request['handle_id'] = handleId
          ws.send(JSON.stringify(request))
        } else {
          Janus.httpAPICall(server + '/' + sessionId + '/' + handleId, {
            verb: 'POST',
            withCredentials: withCredentials,
            body: request
          })
        }
      }
      // Cleanup stack
      config.remoteStream = null
      if(config.volume) {
        if(config.volume['local'] && config.volume['local'].timer)
          clearInterval(config.volume['local'].timer)
        if(config.volume['remote'] && config.volume['remote'].timer)
          clearInterval(config.volume['remote'].timer)
      }
      config.volume = {}
      if(config.bitrate.timer)
        clearInterval(config.bitrate.timer)
      config.bitrate.timer = null
      config.bitrate.bsnow = null
      config.bitrate.bsbefore = null
      config.bitrate.tsnow = null
      config.bitrate.tsbefore = null
      config.bitrate.value = null
      if(!config.streamExternal && config.myStream) {
        Janus.log('Stopping local stream tracks')
        Janus.stopAllTracks(config.myStream)
      }
      config.streamExternal = false
      config.myStream = null
      // Close PeerConnection
      try {
        config.pc.close()
      } catch(e) {
        // Do nothing
      }
      config.pc = null
      config.candidates = null
      config.mySdp = null
      config.remoteSdp = null
      config.iceDone = false
      config.dataChannel = {}
      config.dtmfSender = null
      config.senderTransforms = null
      config.receiverTransforms = null
    }
    pluginHandle.oncleanup()
  }

  // Helper method to munge an SDP to enable simulcasting (Chrome only)
  function mungeSdpForSimulcasting(sdp) {
    // Let's munge the SDP to add the attributes for enabling simulcasting
    // (based on https://gist.github.com/ggarber/a19b4c33510028b9c657)
    var lines = sdp.split('\r\n')
    var video = false
    var ssrc = [ -1 ], ssrc_fid = [ -1 ]
    var cname = null, msid = null, mslabel = null, label = null
    var insertAt = -1
    for(var i=0; i<lines.length; i++) {
      var mline = lines[i].match(/m=(\w+) */)
      if(mline) {
        var medium = mline[1]
        if(medium === 'video') {
          // New video m-line: make sure it's the first one
          if(ssrc[0] < 0) {
            video = true
          } else {
            // We're done, let's add the new attributes here
            insertAt = i
            break
          }
        } else {
          // New non-video m-line: do we have what we were looking for?
          if(ssrc[0] > -1) {
            // We're done, let's add the new attributes here
            insertAt = i
            break
          }
        }
        continue
      }
      if(!video)
        continue
      var sim = lines[i].match(/a=ssrc-group:SIM (\d+) (\d+) (\d+)/)
      if(sim) {
        Janus.warn('The SDP already contains a SIM attribute, munging will be skipped')
        return sdp
      }
      var fid = lines[i].match(/a=ssrc-group:FID (\d+) (\d+)/)
      if(fid) {
        ssrc[0] = fid[1]
        ssrc_fid[0] = fid[2]
        lines.splice(i, 1); i--
        continue
      }
      if(ssrc[0]) {
        var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
        if(match) {
          cname = match[1]
        }
        match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
        if(match) {
          msid = match[1]
        }
        match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
        if(match) {
          mslabel = match[1]
        }
        match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)')
        if(match) {
          label = match[1]
        }
        if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) {
          lines.splice(i, 1); i--
          continue
        }
        if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
          lines.splice(i, 1); i--
          continue
        }
      }
      if(lines[i].length == 0) {
        lines.splice(i, 1); i--
        continue
      }
    }
    if(ssrc[0] < 0) {
      // Couldn't find a FID attribute, let's just take the first video SSRC we find
      insertAt = -1
      video = false
      for(var i=0; i<lines.length; i++) {
        var mline = lines[i].match(/m=(\w+) */)
        if(mline) {
          var medium = mline[1]
          if(medium === 'video') {
            // New video m-line: make sure it's the first one
            if(ssrc[0] < 0) {
              video = true
            } else {
              // We're done, let's add the new attributes here
              insertAt = i
              break
            }
          } else {
            // New non-video m-line: do we have what we were looking for?
            if(ssrc[0] > -1) {
              // We're done, let's add the new attributes here
              insertAt = i
              break
            }
          }
          continue
        }
        if(!video)
          continue
        if(ssrc[0] < 0) {
          var value = lines[i].match(/a=ssrc:(\d+)/)
          if(value) {
            ssrc[0] = value[1]
            lines.splice(i, 1); i--
            continue
          }
        } else {
          var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
          if(match) {
            cname = match[1]
          }
          match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
          if(match) {
            msid = match[1]
          }
          match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
          if(match) {
            mslabel = match[1]
          }
          match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)')
          if(match) {
            label = match[1]
          }
          if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) {
            lines.splice(i, 1); i--
            continue
          }
          if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
            lines.splice(i, 1); i--
            continue
          }
        }
        if(lines[i].length === 0) {
          lines.splice(i, 1); i--
          continue
        }
      }
    }
    if(ssrc[0] < 0) {
      // Still nothing, let's just return the SDP we were asked to munge
      Janus.warn('Couldn\'t find the video SSRC, simulcasting NOT enabled')
      return sdp
    }
    if(insertAt < 0) {
      // Append at the end
      insertAt = lines.length
    }
    // Generate a couple of SSRCs (for retransmissions too)
    // Note: should we check if there are conflicts, here?
    ssrc[1] = Math.floor(Math.random()*0xFFFFFFFF)
    ssrc[2] = Math.floor(Math.random()*0xFFFFFFFF)
    ssrc_fid[1] = Math.floor(Math.random()*0xFFFFFFFF)
    ssrc_fid[2] = Math.floor(Math.random()*0xFFFFFFFF)
    // Add attributes to the SDP
    for(var i=0; i<ssrc.length; i++) {
      if(cname) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' cname:' + cname)
        insertAt++
      }
      if(msid) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' msid:' + msid)
        insertAt++
      }
      if(mslabel) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' mslabel:' + mslabel)
        insertAt++
      }
      if(label) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' label:' + label)
        insertAt++
      }
      // Add the same info for the retransmission SSRC
      if(cname) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' cname:' + cname)
        insertAt++
      }
      if(msid) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' msid:' + msid)
        insertAt++
      }
      if(mslabel) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' mslabel:' + mslabel)
        insertAt++
      }
      if(label) {
        lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' label:' + label)
        insertAt++
      }
    }
    lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[2] + ' ' + ssrc_fid[2])
    lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[1] + ' ' + ssrc_fid[1])
    lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[0] + ' ' + ssrc_fid[0])
    lines.splice(insertAt, 0, 'a=ssrc-group:SIM ' + ssrc[0] + ' ' + ssrc[1] + ' ' + ssrc[2])
    sdp = lines.join('\r\n')
    if(!sdp.endsWith('\r\n'))
      sdp += '\r\n'
    return sdp
  }

  // Helper methods to parse a media object
  function isAudioSendEnabled(media) {
    Janus.debug('isAudioSendEnabled:', media)
    if(!media)
      return true // Default
    if(media.audio === false)
      return false // Generic audio has precedence
    if(media.audioSend === undefined || media.audioSend === null)
      return true // Default
    return (media.audioSend === true)
  }

  function isAudioSendRequired(media) {
    Janus.debug('isAudioSendRequired:', media)
    if(!media)
      return false // Default
    if(media.audio === false || media.audioSend === false)
      return false // If we're not asking to capture audio, it's not required
    if(media.failIfNoAudio === undefined || media.failIfNoAudio === null)
      return false // Default
    return (media.failIfNoAudio === true)
  }

  function isAudioRecvEnabled(media) {
    Janus.debug('isAudioRecvEnabled:', media)
    if(!media)
      return true // Default
    if(media.audio === false)
      return false // Generic audio has precedence
    if(media.audioRecv === undefined || media.audioRecv === null)
      return true // Default
    return (media.audioRecv === true)
  }

  function isVideoSendEnabled(media) {
    Janus.debug('isVideoSendEnabled:', media)
    if(!media)
      return true // Default
    if(media.video === false)
      return false // Generic video has precedence
    if(media.videoSend === undefined || media.videoSend === null)
      return true // Default
    return (media.videoSend === true)
  }

  function isVideoSendRequired(media) {
    Janus.debug('isVideoSendRequired:', media)
    if(!media)
      return false // Default
    if(media.video === false || media.videoSend === false)
      return false // If we're not asking to capture video, it's not required
    if(media.failIfNoVideo === undefined || media.failIfNoVideo === null)
      return false // Default
    return (media.failIfNoVideo === true)
  }

  function isVideoRecvEnabled(media) {
    Janus.debug('isVideoRecvEnabled:', media)
    if(!media)
      return true // Default
    if(media.video === false)
      return false // Generic video has precedence
    if(media.videoRecv === undefined || media.videoRecv === null)
      return true // Default
    return (media.videoRecv === true)
  }

  function isScreenSendEnabled(media) {
    Janus.debug('isScreenSendEnabled:', media)
    if (!media)
      return false
    if (typeof media.video !== 'object' || typeof media.video.mandatory !== 'object')
      return false
    var constraints = media.video.mandatory
    if (constraints.chromeMediaSource)
      return constraints.chromeMediaSource === 'desktop' || constraints.chromeMediaSource === 'screen'
    else if (constraints.mozMediaSource)
      return constraints.mozMediaSource === 'window' || constraints.mozMediaSource === 'screen'
    else if (constraints.mediaSource)
      return constraints.mediaSource === 'window' || constraints.mediaSource === 'screen'
    return false
  }

  function isDataEnabled(media) {
    Janus.debug('isDataEnabled:', media)
    if(Janus.webRTCAdapter.browserDetails.browser === 'edge') {
      Janus.warn('Edge doesn\'t support data channels yet')
      return false
    }
    if(media === undefined || media === null)
      return false // Default
    return (media.data === true)
  }

  function isTrickleEnabled(trickle) {
    Janus.debug('isTrickleEnabled:', trickle)
    return (trickle === false) ? false : true
  }
}

export default Janus
